[BUG] Auto-update enters unrecoverable restart loop: single-stream download with no resume times out and discards all progress
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
- No resume. Attempt B lost 102 MB of completed download on timeout. The server supports range requests; the client does not use them.
- Self-competition.
claude updateand the in-session background fetch download the same artifact concurrently, roughly halving each other's throughput and making the timeout more likely to fire. - 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.
- 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.
- 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.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗