[BUG] Auto-update enters unrecoverable restart loop: single-stream download with no resume times out and discards all progress

Status Open
Reported on v2.1.220
Maintainer reply ✓ Yes — bcherny
Activity 5 comments · opened Aug 8, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Bug Description

On a connection where the update binary takes longer to download than the updater's timeout, claude update enters an unrecoverable restart loop. Each attempt downloads from scratch, times out partway, and is replaced by a fresh attempt that also starts at zero. The update can never complete, and each cycle leaks a partial download into the staging cache.

The root cause is that the downloader uses a single connection and does not use HTTP range requests to resume, even though the CDN explicitly advertises accept-ranges: bytes.

Environment

  • Platform: darwin 26.6, arm64
  • Installed version: 2.1.220 (native installer, ~/.local/bin/claude → ~/.local/share/claude/versions/<ver>)
  • Target version: 2.1.226
  • Artifact: 279,661,952 bytes (267 MiB)

Measurements

Single-stream throughput from the release bucket, sampled over 10s:

claude update (PID 17718): 140 KB/s
background session fetch:  126 KB/s

Eight parallel HTTP range requests against the exact same URL, same machine, same minute:

907 KB/s   (6.5x faster)

The link is not the bottleneck — the per-connection rate is. curl to an unrelated host measured 576 KB/s while both updaters were still running, i.e. the pipe sustains ~7.3 Mbit total.

At 140 KB/s a 267 MiB artifact needs ~32 minutes, which exceeds the updater's timeout, so the download is structurally guaranteed to fail on any connection at or below roughly 2 Mbit per stream.

Observed failure sequence

16:08  attempt A starts             -> stalls at 51 MB, process dies, dir orphaned
16:08  attempt B starts             -> reaches 102 MB (38%), times out, ALL progress discarded
16:16  session background fetch     -> starts at 0, competes for the same pipe
16:21  attempt C spawns after B     -> starts at 0 again

Three cycles, ~270 MB of traffic transferred, 0% installed, still on 2.1.220.

Consequences

  1. No resume. Attempt B lost 102 MB of completed download on timeout. The server supports range requests; the client does not use them.
  2. Self-competition. claude update and the in-session background fetch download the same artifact concurrently, roughly halving each other's throughput and making the timeout more likely to fire.
  3. Staging cache leak. Each dead attempt leaves its partial download behind. Accumulated here across one afternoon:

``
~/.cache/claude/staging/2.1.226.17166.1786194490387/claude 51 MB
~/.cache/claude/staging/2.1.226.27280.1786195247074/claude 31 MB
~/.cache/claude/staging/2.1.226.23753.1786194970471/claude 68 MB
``
Nothing reaps these; they grow without bound across failed attempts.

  1. Zero-byte version stub. A failed attempt left an empty file at the destination path:

``
-rw-r--r-- 0 Aug 8 15:59 ~/.local/share/claude/versions/2.1.226
``
This sits exactly where the real binary belongs, and is a plausible source of "update succeeded but nothing changed" and checksum-mismatch reports.

  1. No progress indication. The user sees only an eventual timeout, with no signal that ~100 MB was transferred and thrown away.

Suggested fixes

  • Use HTTP range requests with resume (Range: + -C - semantics). The bucket already supports it. This alone fixes the loop.
  • Use several parallel connections. Measured 6.5x speedup on this link.
  • Scale the timeout to bytes-transferred rather than wall-clock, or reset it on progress, so a slow-but-healthy download is not killed.
  • Take a lock so only one updater runs per version at a time.
  • Reap staging directories whose owning PID is no longer alive.
  • Write the version binary atomically (temp file + rename) so a failure cannot leave a 0-byte file at the destination.

Workaround

Fetching manually with 8 parallel range requests and verifying against the bucket's published MD5 completed in ~5 minutes on the same connection that the built-in updater could not finish in 30.

Possibly related: #73596, #81811 (checksum mismatch crash — a 0-byte or truncated staged file would produce exactly that), #80147.

View original on GitHub ↗

3 Comments

bcherny collaborator · 1 month ago

Thanks for the exceptionally thorough report — this reproduces.

Verified against the current release (2.1.233, Linux):

  • The release CDN does advertise accept-ranges: bytes (a test Range request returns 206), and the updater does not use range requests — a download that dies partway always restarts from byte 0.
  • The updater has a fixed total download deadline, and hitting it is terminal: with the deadline shortened to simulate a slow link, claude install fails with Download timed out: exceeded the total deadline and all transferred bytes are discarded. On a link where the artifact can't finish inside that window, the update is structurally unable to complete, exactly as you describe.
  • Each failed attempt leaves an orphaned per-attempt directory under ~/.cache/claude/staging/<version>.<pid>.<timestamp> — we observed them accumulating across attempts with nothing cleaning them up.
  • We also reproduced the 0-byte file at ~/.local/share/claude/versions/<version> after a failed attempt. One mitigating note: the installer does not treat that stub as a valid install, so a later successful attempt overwrites it.

One improvement that shipped after your report: as of 2.1.243 the update binary is zstd-compressed (roughly 75 MB instead of ~340 MB on Linux x64), which substantially shrinks the window in which a slow link hits the deadline — but it does not add resume, so the underlying failure mode you identified remains on sufficiently slow connections.

Flagging this to the team as a bug with your suggested fixes (resume via range requests, progress-based rather than wall-clock deadline, staging cleanup).

🤖 Generated with Claude Code

zaz · 1 month ago

Confirming this on Linux x86_64 (Debian 13), 2.1.241 → 2.1.246 — i.e. after the 2.1.243 zstd change: the ~75 MB compressed artifact shrinks the window but on a sufficiently slow route (~13 KB/s per connection here) claude update still hits the deadline and discards all progress, with the same terminal error:

Error: Failed to install native update
TelemetrySafeError: Download timed out: exceeded the total deadline

We also reproduced the 0-byte stubs at ~/.local/share/claude/versions/<version> (two of them, from separate failed attempts).

Working through the manual paths surfaced two additional failure layers in the same pipeline that I don't believe are filed anywhere:

1. install.sh's fallback goes from 78 MB to 248 MB and deletes the partial. The official installer (claude.ai/install.sh) downloads claude.zst with plain curl -fsSL — no --retry, no -C -. When our transfer got a connection reset at ~35 MB (45%), the script ran rm -f "$binary_path.zst" and fell back to downloading the uncompressed 248 MB binary from byte 0 (lines ~199–219). That fallback makes sense for zstd/decompression problems, but for a network failure it responds to "the link couldn't finish 78 MB" by attempting 3.2× the data — undoing the 2.1.243 improvement exactly when it matters most. The fallback should retry/resume the smaller artifact and only switch to the uncompressed one on decompression failure.

2. claude install <version> re-downloads a binary it already is. After manually downloading and SHA256-verifying claude-2.1.246-linux-x64, running ./claude-2.1.246-linux-x64 install 2.1.246 (install.sh's final step) opened a fresh connection and began re-downloading the full artifact into ~/.cache/claude/staging/ rather than copying itself. Installed version files are byte-identical to the published artifacts (verified: our installed 2.1.241 matches its manifest size and checksum exactly), so a self-copy — or accepting a local checksum-verified file — would eliminate ~250 MB of redundant transfer per install on constrained links.

Corroborating your parallelism numbers on a different network: 1 connection ≈ 13 KB/s; 8 parallel range requests against the same URL ≈ 94–150 KB/s aggregate (7–11×). A manual ranged download with per-chunk resume survived three connection resets and completed in ~15 minutes, verified against both manifest checksums, on the same link where the official paths failed for ~90 minutes. Range support plus the published SHA256s mean everything needed for resumable, verifiable downloads already exists server-side.

Suggested additions to the fix list already flagged:

  • claude install should install from its own bytes (or a local verified file) when the running binary matches the target version/checksum.
  • install.sh should resume the .zst on network failure and keep the partial, reserving the uncompressed fallback for decompression errors.

🤖 Generated with Claude Code

marcindulak · 24 days ago

For your information: the label reproduced on this issue does not prevent it from becoming stale and eventually auto-closed. See https://github.com/anthropics/claude-code/issues/87647 for details, and consider upvoting that for visibility.

Showing cached comments. Read the full discussion on GitHub ↗