Auto-updater downloads the full ~265MB update once per running session (no cross-session lock); failed attempts leave 0-byte version files

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

Summary

The auto-updater has no cross-session coordination: when a new version ships, every running session independently downloads its own full copy of the ~265 MB binary. With worktree/multi-session workflows (which Claude Code's own features encourage — worktrees, background agents, subagents), one point release costs N × 265 MB of bandwidth. Failed attempts also leave corrupt artifacts behind that trigger the "✗ Auto-update failed · Try claude doctor" banner.

Environment

  • Claude Code 2.1.216, native installer (installMethod: native), Linux
  • 6 concurrent interactive sessions (multiple git worktrees)
  • Release 2.1.217 became available

Observed behavior

Within ~12 minutes of the release, each session spawned its own independent download into a per-PID staging dir:

$ ls ~/.cache/claude/staging/
2.1.217.2390685.1784670154232   (empty — failed attempt)
2.1.217.2390685.1784670805043   (partial, actively downloading)
2.1.217.2400861.1784671076329   (partial, actively downloading)
2.1.217.2485424.1784670423976   (stalled at 79 MB)
2.1.217.2619776.1784670551958   (stalled at 64 MB)
2.1.217.2952780.1784670891404   (partial, actively downloading)
2.1.217.3248443.1784670208495   (empty — failed attempt)

At peak, 3 downloads were running simultaneously (~600 MB in flight for one machine). lsof confirmed three distinct claude processes each holding their own staging file open.

Additionally, an earlier failed attempt left a 0-byte version file:

$ stat -c '%y %s %n' ~/.local/share/claude/versions/2.1.217
2026-07-21 14:42:34 0 /home/.../claude/versions/2.1.217

and that session displayed ✗ Auto-update failed · Try claude doctor.

Expected behavior

  1. Cross-session download lock — e.g., a lockfile under ~/.local/share/claude/ so the first session to notice the release downloads it and other sessions wait or skip (they all install to the same shared versions/ dir anyway).
  2. Atomic install — download to staging, verify, then rename into versions/. A failed attempt should never leave a 0-byte versions/<v> entry.
  3. Stale staging cleanup — empty/stalled staging dirs from failed attempts accumulate and are never removed.

Impact

  • Metered/limited-bandwidth connections pay N× for every release (releases are frequent).
  • Redundant downloads compete with each other, increasing the chance each one stalls/fails — which is how the corrupt 0-byte version file and the "Auto-update failed" banner were produced in the first place.

Workaround

"env": {"DISABLE_AUTOUPDATER": "1"} in settings.json plus manual claude update from a single session.

View original on GitHub ↗

5 Comments

kcarriedo · 1 month ago

The multi-session download problem you are describing compounds in a way that is not obvious from the issue title. The bandwidth multiplication is the visible symptom, but the deeper issue is that there is no shared coordination layer at all between concurrent sessions — each session is effectively isolated and has no way to learn that a sibling has already handled a shared lifecycle event like an update.

That same isolation shows up in other places too: no shared state for permission grants, no way for one session to yield to another when they are both about to touch the same file, no mechanism for a parent session to learn that a background subagent finished. The auto-updater is hitting this because it was built with single-session assumptions, and those assumptions are now baked into a lot of places in the codebase.

A workaround for the immediate bandwidth issue: the sessions can be made to coordinate through a simple lockfile in a well-known location (e.g., $XDG_CACHE_HOME/claude-code/update.lock or a path relative to the project root). The first session to acquire the lock downloads and installs; others check the installed version on a short poll interval and skip their download once the version file reflects the new release. Not pretty, but it sidesteps the per-session download storm until there is a first-party fix.

The longer-term fix probably requires a lightweight coordinator process that all sessions in the same user context connect to — something that can own the update lifecycle, the permission approval queue, and eventually cross-session messaging. That is a larger architectural change but the auto-updater case is a good first target because it is discrete and the behavior is clearly wrong.

robklingler · 8 days ago

Reproduced a variant of this on Linux, native installer, single machine (2 running claude processes: a long-lived remote-control process plus one interactive session — not a multi-worktree/multi-session scenario).

Instead of N sessions each downloading once, the same update kept retrying indefinitely: ~/.local/share/claude/versions/2.1.239 and 2.1.240 are both 0-byte, while ~/.cache/claude/staging/ had accumulated 233 staged attempts (~60GB) at ~280MB each, one created roughly every 5 minutes over ~19 hours (2.1.239.* starting ~2026-08-21 17:10 through 2.1.240.* at ~2026-08-22 11:10, still running when found). lsof showed no open handles on any of them — every attempt had already failed and been abandoned, but nothing ever cleaned them up or backed off. This ran root filesystem usage up to 70% and tripped a disk-space monitoring alert before it was caught.

Confirms the two fixes requested here — atomic install (no 0-byte versions/ entries on failure) and stale-staging cleanup — would also cover this single-session retry-loop variant, not just the concurrent-session case.

robklingler · 7 days ago

Following up on my comment above — I chased this further and got a couple of things wrong, so correcting the record. The short version: my environment description was incomplete, and one of the behaviours I said was missing turns out to exist.

What I got wrong

1. The trigger wasn't the two processes I described. I said "2 running claude processes: a long-lived remote-control process plus one interactive session." That was the wrong culprit. The actual driver was a cron job running every 5 minutes that spawns a short-lived claude TUI (a dashboard tool scraping the /usage panel). I'd overlooked it because it isn't a session I sit in.

2. So "kept retrying indefinitely… nothing ever backed off" mischaracterises it. There's no retry loop here and nothing for Claude Code to back off from — each 5-minute invocation was a fresh process making its first attempt. The regular cadence I reported was just my cron's schedule. I don't think there's a backoff defect, and I shouldn't have implied one.

3. Most importantly — staging cleanup does exist. I said "nothing ever cleaned them up." That's wrong. Claude Code does garbage-collect the staging directory, but as far as I can tell only on a successful update. Once one succeeded, staging went from 246 directories to 12 on its own (it appears to keep roughly the last hour). Since no update ever succeeded on this machine, the cleanup never got a chance to run. So the request isn't "add cleanup" — it's "also clean up on failure or at startup," which is a smaller ask.

What I now think was actually happening

Every staged file was a different size (130–297 MB), and none ever reached the real binary size (2.1.241 is 342,636,848 bytes). Comparing each staging directory's mtime to the mtime of the file inside it, each download ran only 2–3 seconds before stopping.

My reading — and I'm less confident here than in the facts above — is that the download begins roughly 21 seconds into a session and needs ~3.5s at my line rate, while the scraper tears its session down at about 24 seconds. So it misses completing by something on the order of a second, leaves a partial file, and the next cron tick starts over from scratch.

That would make it a threshold effect rather than a hard failure, which fits the version history: installs succeeded through 2.1.238 and only started failing afterwards, as the binary grew (2.1.222 ≈ 289 MB → 2.1.238 ≈ 339 MB → 2.1.241 ≈ 343 MB). It also means link speed and binary size both determine whether a given setup trips it.

The test that convinced me: manually holding one pty session open for 35 seconds let the download finish, 2.1.241 installed cleanly, and the self-cleanup described above fired. Disk went from 65 GB free back to 124 GB.

What I think still stands from the original issue

Both of the fixes requested here would have prevented this, and I'd still support them:

  1. Atomic install — a failed attempt should never leave a 0-byte entry in versions/. I had 0-byte stubs for 2.1.239, 2.1.240 and 2.1.241 sitting next to a working 2.1.238.
  2. Staging cleanup that also runs on failure, not only after a success.

One additional thought, offered tentatively: a process that is about to exit imminently arguably shouldn't start a ~340 MB download it can't finish — or the partial should be resumable or discarded on exit rather than left behind. That may be out of scope for this issue, which is really about cross-session coordination; my variant isn't a concurrency problem at all, so feel free to tell me it belongs somewhere else.

Unverified

  • The ~21s/~3.5s/~24s timing above is inferred from file mtimes, not from instrumenting the updater.
  • I never reproduced this on macOS or Windows, or on a non-native install.
  • I couldn't negative-test DISABLE_AUTOUPDATER=1: by the time I tried, my install was already current, so no download is attempted either way.
  • I've since removed the 5-minute claude spawn from my own setup entirely, so I can't easily re-trigger this on demand any more.
sh-at-cs · 6 days ago

While helping me diagnose #89262, Claude found some new information pertaining to this ticket here.

[!WARNING] 🤖 LLM-generated wall of text below

Correction/update: The cross-session lock already (or by now?) exists — it just expires halfway through the download.

The 2.1.241 linux-x64 updater takes a wx-flagged, PID-containing lock at ~/.claude/.update.lock before any install work. A session finding it held logs Another process is currently installing an update, returns status in_progress, and emits a tengu_auto_updater_lock_contention telemetry event — so simultaneous starts already resolve correctly, and contention is already instrumented.

The defect: the stale-lock threshold is 300000 ms (5 min, judged by the lock's mtime) while the download deadline is 600000 ms (10 min), and the mtime is stamped once at creation and never refreshed. Any download slower than 5 minutes therefore has its own lock deleted as abandoned while it is still downloading normally; the next session takes a fresh lock and starts a second full download, the first process never learns, and it repeats per session. Hence the per-PID staging directories in this issue.

For the current 342,636,848-byte binary:

| Sustained bandwidth | Outcome |
| --- | --- |
| above ~9.14 Mbit/s | finishes inside 5 min, lock holds, single download |
| ~4.57 – 9.14 Mbit/s | would finish inside the deadline unopposed, but the lock goes stale mid-download, sessions pile in, and the split bandwidth pushes it past 10 min |
| below ~4.57 Mbit/s | fails even unopposed (#89262) |

The pile-on starts at twice the bandwidth the deadline alone implies, which is why this also hits people whose connections are fine.

Reproduced by raising CLAUDE_CODE_DOWNLOAD_DEADLINE_MS_FOR_TESTING above the default, starting a single manual update on a connection slow enough that the download outlasts five minutes, and watching ~/.cache/claude/staging/. Fresh 2.1.241.<pid>.<ts> directories from other sessions begin appearing a few minutes after the lock's five-minute lifetime elapses — not at the download's start — and keep arriving while it runs. They aren't racing the start; they're walking in through an expired lock. Each is left behind empty when its own attempt fails (partials get deleted, directories don't).

This is more severe than "redundant downloads": the contention is what pushes each download past the deadline, so under ~9 Mbit/s with more than one session open, updates stop succeeding at all and the failure banner sticks. #85046 measures the same self-competition from the other side (140 KB/s single-stream vs 907 KB/s over 8 parallel ranges).

Fix: refresh the lock's mtime while bytes are arriving — the downloader already has a per-chunk callback driving stall detection. Failing that, put the staleness threshold above the deadline. Better, per #85046, make both byte-progress-based: the deadline and the lock staleness are the same wall-clock mistake made twice.

Related: #89262 (the deadline itself), #85046, #86970 (no Range resume; binary now 342.6 MB), #66751 (same deadline in June, closed by the inactivity bot).

<sub>Read from the shipped 2.1.241 linux-x64 binary — minified, so behaviour only, no source quotes; the constants and the absent mtime refresh are directly observable in it.</sub>

bcherny collaborator · 5 days ago

Reproduced on 2.1.233 (Linux, native install).

  • Running two claude install <version> processes concurrently produced two independent staging directories under ~/.cache/claude/staging/ (named <version>.<pid>.<timestamp>, matching your listing), each downloading the full ~300 MB binary at the same time — so there is indeed no cross-session/cross-process download coordination, and N sessions will download N copies.
  • After the downloads failed mid-transfer (flaky network), ~/.local/share/claude/versions/<version> was left behind as a 0-byte file, reproducing the corrupt-artifact state that leads to the "✗ Auto-update failed · Try claude doctor" banner.
  • The empty staging directories from the failed attempts were left in place and never cleaned up.

The DISABLE_AUTOUPDATER=1 + manual claude update workaround is a reasonable mitigation for multi-session/worktree setups in the meantime.

🤖 Generated with Claude Code