[BUG] Remote Control sessions die after ~20 min idle — server TTL ignores keepalives
Environment
- Claude Code: v2.1.72
- OS: macOS (Darwin 25.2.0)
- Plan: Max
- Reproduced on: interactive CLI sessions (
/remote-control), auto-RC (remoteControlAtStartup: true), and agent sessions (--agent)
Description
All Remote Control sessions silently die after ~5-30 minutes of idle time (most commonly ~20 minutes). The phone app shows "Failed to send message — An unknown network error has occurred" while the local CLI still displays "Remote Control active." The session_ingress endpoint returns HTTP 404 — the session is deregistered server-side while the CLI process is alive, connected, and polling.
This affects every RC user who steps away from their keyboard — the core use case of "start at your desk, continue from your couch."
Root Cause (source-verified against cli.js v2.1.72)
We identified two independent bugs that together leave idle RC sessions unprotected:
Bug 1: Server-side session TTL does not reset on keepalive messages
The WebSocket transport sends {"type":"keep_alive"} data frames every 5 minutes via startKeepaliveInterval() (300,000ms interval, Swz=300000). The WebSocket ping/pong mechanism also runs every 10 seconds (hwz=10000).
Neither mechanism prevents server-side session deregistration. We verified this empirically:
- Sessions with the 5-minute keepalive actively sending
keep_aliveframes still die at ~20 minutes - Sessions with 10-second WebSocket pings running continuously still die at ~20 minutes
- The server-side session TTL appears to only reset on real user/model activity (actual messages through the bridge), not on transport-level keepalive frames
Note on CLAUDE_CODE_REMOTE: When set (e.g., in standalone claude remote-control bridge mode), CLAUDE_CODE_REMOTE disables the 5-minute keepalive entirely via an early return in startKeepaliveInterval(). However, even when the keepalive IS running (interactive /remote-control sessions), sessions still die — confirming the server ignores these frames for TTL purposes.
// startKeepaliveInterval() — 5-min keepalive (runs for interactive sessions)
startKeepaliveInterval() {
this.stopKeepaliveInterval();
if (t6(process.env.CLAUDE_CODE_REMOTE)) return; // skipped in standalone bridge mode
this.keepAliveInterval = setInterval(() => {
this.ws.send(JSON.stringify({type: "keep_alive"}) + "\n");
}, 300000); // 5 minutes
}
Bug 2: SEND_KEEPALIVES replacement is broken by refcount gating
CLAUDE_CODE_REMOTE_SEND_KEEPALIVES was designed as an additional keepalive mechanism that sends {"type":"keep_alive"} every 30 seconds through the transport. However, the keepalive interval is gated by a refcount (C36) that tracks active model processing:
// Refcount increment — called when tool execution or streaming starts
function DD1() {
C36++;
if (C36 === 1) ie7(); // start 30s keepalive interval
}
// Refcount decrement — called when tool execution or streaming ends
function XD1() {
C36--;
if (C36 === 0) {
if (Dd !== null) { clearInterval(Dd); Dd = null; } // ← CLEARS the keepalive interval
kE9(); // start idle timer (informational only)
}
}
// The 30s keepalive interval
function ie7() {
if (Dd !== null) { clearInterval(Dd); Dd = null; } // clear previous
if (mg6 !== null) { clearTimeout(mg6); mg6 = null; } // clear idle timer
Dd = setInterval(() => {
if (t6(process.env.CLAUDE_CODE_REMOTE_SEND_KEEPALIVES))
I36?.(); // sends {"type":"keep_alive"} via registered callback
}, 30000); // 30 seconds
}
Visual summary of all three keepalive paths:
[RC Keepalive Architecture — 3 mechanisms, all broken during idle]
<img width="1800" height="1360" alt="Image" src="https://github.com/user-attachments/assets/dfc8b00f-40e0-47bb-8780-2e4d24420ddf" />
The flow:
- Model starts processing →
DD1()incrementsC36to 1 →ie7()starts the 30s keepalive interval - Model finishes processing →
XD1()decrementsC36to 0 →clearInterval(Dd)kills the keepalive - Session is now idle with the
SEND_KEEPALIVESmechanism stopped
The keepalive only runs while the model is actively processing — exactly when it's NOT needed. It stops during idle — exactly when sessions die.
We verified this empirically: sessions spawned with CLAUDE_CODE_REMOTE_SEND_KEEPALIVES=1 still died at ~25-30 minutes.
Bridge heartbeat — DISABLED server-side
// Server response from tengu_bridge_poll_interval_config:
{
"poll_interval_ms_not_at_capacity": 2000,
"poll_interval_ms_at_capacity": 600000,
"heartbeat_interval_ms": 0
}
The bridge-level heartbeat infrastructure exists in the code (heartbeatWork() API call, poll loop heartbeat mode) but is server-disabled (heartbeat_interval_ms: 0).
Reproduction
- Start Claude Code:
claude - Enable RC:
/remote-control - Connect from Claude iOS/Android app
- Send one message to confirm connectivity
- Leave both sides completely idle
- Wait ~20 minutes
- Try sending a message from the phone → "Failed to send message — An unknown network error has occurred"
- The CLI still shows "Remote Control active"
Reproduction rate: 100% across 7 independent sessions tested (both relay agent sessions and normal interactive sessions).
Timeline observations:
| Session | Type | Age at death |
|---------|------|-------------|
| Interactive (auto-RC) | remoteControlAtStartup | ~21 min |
| Agent relay #1 | --agent with /remote-control | ~20 min |
| Agent relay #2 | --agent with /remote-control | ~20 min |
| Agent relay #3 | --agent with SEND_KEEPALIVES=1 | ~25 min |
| Agent relay #4 | --agent with SEND_KEEPALIVES=1 | ~30 min |
| Agent relay #5 | --agent with /remote-control | ~25 min |
| Agent relay #6 | --agent with /remote-control | ~25 min |
Control test: 75 messages in 5 minutes to a fresh session — survived fine. Message volume does not cause the drop. It is purely time-based idle death.
Additional evidence: Curling the session_ingress endpoint directly returns HTTP 404 while the local CLI process is alive with TCP ESTABLISHED connections. The server deregisters the session before TCP teardown — the CLI never detects the loss.
Suggested Fix
Bug 1 (server-side): The server should count keep_alive messages (or WebSocket pings) as session activity for TTL purposes. Alternatively, enable heartbeat_interval_ms server-side (set to e.g. 30000) — the client-side heartbeat infrastructure already exists and runs unconditionally.
Bug 2 (client-side, one-line fix): Don't clear the keepalive interval when SEND_KEEPALIVES is set. In XD1(), skip clearing Dd when CLAUDE_CODE_REMOTE_SEND_KEEPALIVES is truthy:
function XD1() {
C36--;
if (C36 === 0) {
if (!t6(process.env.CLAUDE_CODE_REMOTE_SEND_KEEPALIVES)) {
if (Dd !== null) { clearInterval(Dd); Dd = null; }
}
kE9();
}
}
This would make the 30-second keepalive run continuously during idle. Combined with Bug 1's fix (server counts keepalives as activity), idle sessions would survive indefinitely.
Current Workaround
The only effective mitigation is triggering periodic real model activity (e.g., sending a trivial message via terminal input every ~15 minutes). This resets the server-side session TTL because it generates actual messages through the bridge transport.
We also patched cli.js to restore the 5-minute keepalive (removing the CLAUDE_CODE_REMOTE guard from startKeepaliveInterval) — sessions still died at ~20 minutes, confirming the server-side TTL is the primary bug. Client-side keepalive fixes alone are insufficient.
Impact
This affects every Remote Control user. The core marketing promise — "start a task at your desk, then pick it up from your phone on the couch" — breaks the moment you set your phone down for 20 minutes.
Related Issues
These all describe symptoms of this root cause:
- #28571 — Session fails to resync after connection drop
- #28914 — Connection dies overnight while tmux stays alive
- #29313 — Sessions go stale mid-conversation
- #28532 — Frequent disconnections requiring page refresh
- #29726 — iOS app drops on background/foreground
- #28402 — Session not visible in session list
- #29219 — Connection fails after rate limit
- #32651, #32746, #32833 — Recent reports of shorter (~5-15 min) idle death windows
Supporting downstream evidence: Henderson11 on #28571 traced WebSocket close code 1002 (protocol error) occurring after the disconnect — this is the downstream effect of the session expiring server-side. Our analysis identifies the upstream cause: the server-side TTL that ignores keepalive traffic, compounded by the SEND_KEEPALIVES refcount bug that disables the only mechanism intended to address this.
Research Methodology
- Static analysis of
/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js(v2.1.72, 12MB minified) - Function-chain tracing through minified code:
DD1→XD1→C36→ie7→I36→keep_alive - Empirical testing: 7 sessions across 4 configurations (interactive, auto-RC, agent relay, SEND_KEEPALIVES=1)
- Verified
SEND_KEEPALIVES=1doesn't fix idle death (refcount drops to 0 between turns) - Verified cli.js patch removing
CLAUDE_CODE_REMOTEguard doesn't fix idle death (server ignores transport-level keepalives for TTL) - Verified on normal interactive session (
remoteControlAtStartup) — not specific to agent/relay usage - Cross-referenced with 10+ existing GitHub issues (all symptom reports, no root cause analysis)
19 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
This is not a duplicate. The linked issues (#31853, #32833, #28914) are symptom reports — they describe that sessions die but not why.
This issue identifies two specific code-level bugs in the keepalive system with source-verified root cause analysis:
startKeepaliveInterval,Swz=300000) and 10s WS ping (hwz=10000) are both active, but the server-side session registry does not count them as activity. Empirically verified — sessions with keepalives actively sending still die at ~20 min.SEND_KEEPALIVESrefcount gating: The 30s application keepalive is gated by a refcount (C36) that tracks active model processing.DD1()starts the interval when a tool/stream begins;XD1()callsclearInterval()when it ends. The keepalive only runs during active model turns — not during idle, which is exactly when sessions die. Empirically verified — sessions withCLAUDE_CODE_REMOTE_SEND_KEEPALIVES=1still die at ~25-30 min.heartbeat_interval_ms: 0in thetengu_bridge_poll_interval_configresponse. The infrastructure exists but is turned off.None of the linked issues contain this analysis. They have zero Anthropic responses despite 40+ combined upvotes and months of reports across 10+ duplicate issues.
Three concrete fixes with code are provided in the issue body.
UPDATE: This appears to be fixed in v2.1.74! 🎉
Traced through cli.js in v2.1.74 and found a new 4th keepalive mechanism that wasn't present in v2.1.72:
session_keepalive_interval_ms— 2-minute session keepaliveKey properties:
CLAUDE_CODE_REMOTE(unlike mechanism #1 in the original report)Test results: RC session survived 30+ minutes idle on v2.1.74 with zero intervention. Previously failed 100% of the time at ~20 min on v2.1.72.
The 3 original mechanisms from this report are unchanged — the WS keepalive still returns early for RC, SEND_KEEPALIVES is still refcount-gated, heartbeat_interval_ms is still 0. The fix was adding a clean new mechanism that bypasses all three issues. Smart approach.
---
Incredible turnaround by the team — shipped within a day. @noahzweben, @ashwin-ant, and the rest of the Claude Code team — thank you for the fast response. This makes Remote Control genuinely usable for the "code from your couch" workflow.
Happy to close this issue if the fix is confirmed intentional. For anyone else hitting this — update to v2.1.74.
Found this from your reddit thread, kudos for not giving up on fixing this!
I don't think it's been solved. I'm still
unable to connect from my remote sessions. I have a service that tries keeping alive a session on a repo on a particular machine for me to work on when I'm not in front of the computer, but by the time I need it. It always hangs. Suggesting leaving open for now?
Remote sessions (started on claude.ai/code) die then can't be recovered
2.1.87 (Claude Code)
Linux Ubuntu. It does not work after 20 minutes etc... Useless.
Still happening all the time to me. Running ubuntu VM that has 0 downtime. Remote connections always disconnect.
Same on Linux (Ubuntu 22.04, Claude Code on a shared HK server, Pro plan). Suggesting
platform:linuxlabel since the currentplatform:macosundercounts coverage.One additional behavioral detail not yet documented in this thread — the desktop/mobile split:
This breaks the documented "start at your desk, continue from your couch" promise even for users who do eventually return to their desk — the mobile side is effectively read-only for any session that crossed the ~20 min TTL boundary.
Any ETA to fix it?
Come on. Pretty please...?
Confirming this still bites on v2.1.150 (self-hosted
claude remote-controlunder systemd), and I traced exactly why for my account.The v2.1.74 fix (the 4th keepalive,
session_keepalive_interval_v2_ms, code default120000, not refcount-gated) is present in my binary. The problem is the value resolution: it comes from the GrowthBook flagtengu_bridge_poll_interval_config, and the server pushessession_keepalive_interval_v2_ms: 0to my account. That trips theif (M > 0)guard, so the keepalive timer never starts and RC sessions archive after ~15 min idle.Resolver in v2.1.150:
oLH()reads the GrowthBook flag with the code default{ ..., session_keepalive_interval_v2_ms: 120000 }. My cached value is0, and it gets rewritten to0on every flag refresh (I set it to30000locally, a single client start reset it back to0).What does not work as a user-side fix:
~/.claude.json. Overwritten on the next GrowthBook refresh.DISABLE_GROWTHBOOK=1. This does make the resolver fall back to the120000default (v$returns the passed default when GrowthBook is off), but Remote Control itself is GrowthBook-gated, so the host then exits withError: Remote Control is not yet enabled for your account. The keepalive and the RC-enable gate are coupled to the same switch.qf$/Kf$) are not wired to any input in this build, so there is no surgical way to force just this one flag.Net result: for any account where the server sends
session_keepalive_interval_v2_ms: 0, RC is enabled but unusable past the idle timeout, with no client-side workaround.Could the team either flip this flag's value for affected accounts, or decouple the RC-enable gate from
DISABLE_GROWTHBOOKso the code default keepalive can be used when GrowthBook is off? Happy to share more of the trace if useful.This is still an issue.
Another data point confirming @Ashkaan's diagnosis that this is now an account-level flag rollout problem, not a client bug — verified on v2.1.220.
Environment: Claude Code v2.1.220, macOS (Darwin 25.4.0), Max plan.
Server-pushed flag values (cached
tengu_bridge_poll_interval_configin~/.claude.jsonfor my account):Binary check: the v2.1.220 binary still contains the v2.1.74 fix with its schema default intact —
— and the same guarded resolver Ashkaan traced in v2.1.150:
So for any account served
0, the keepalive timer never starts, every other keepalive path is also disabled (heartbeat_interval_ms: 0, plus the two mechanisms from the original report), and RC sessions are deregistered server-side after ~15–20 min idle. Symptoms match the original report exactly: mobile shows "Failed to send message — An unknown network error has occurred", the CLI still shows "Remote Control active", and the dead session cannot be revived from mobile.As Ashkaan noted, there's no client-side workaround: editing the cached value is overwritten on the next GrowthBook refresh, and
DISABLE_GROWTHBOOK=1disables the RC-enable gate itself.Request: please flip
session_keepalive_interval_v2_msto a positive value for affected accounts (the code default of 120000 already works — v2.1.74 proved that for accounts that receive it), or decouple the RC-enable gate from GrowthBook so the code default can apply.Still present in v2.1.233 — the refcount gate is unchanged, and the client now volunteers its idleness to the server
Confirming this on v2.1.233 (macOS, Darwin 25.6.0, Max plan, interactive
/remote-controlsessions). The original analysis was against v2.1.72; both bugs survive ~160 releases later, and reading the current binary turned up two things that sharpen the report.1. The refcount gate in Bug 2 is unchanged in v2.1.233
Same shape, different minified names:
So the keepalive interval still exists only while a turn is running, and even then only sends when
CLAUDE_CODE_REMOTE_SEND_KEEPALIVESis set. An idle session — the entire population this issue is about — has no keepalive timer at all.The telemetry event name
session_keepalive_heartbeatcarriesrefcountas its only attribute, which should make the gating visible in your own telemetry: for sessions that later die of idleness, that event stops before the death rather than continuing at 30s.2. The client actively advertises its idleness, so this is not just "unprotected" — it is opted into
Every heartbeat carries an
idle_secondsthe client computes itself:sent as:
The server answers with an idle grant the client latches (
idleGrantLatched, surfaced throughisIdleAdvised()), and the heartbeat interval stretches accordingly.idle_secondsis derived purely from turn activity — there is no user-presence, terminal, or viewer input to it. A session deliberately parked waiting for a remote prompt is indistinguishable, on the wire, from one that has been abandoned. That is the crux: the one thing Remote Control exists to support (walk away from the desk, drive it from a phone) is the exact state the heartbeat protocol reports as "nothing happening here".Suggested fixes, in preference order
idle_secondsshould bemin(turn_idle, remote_presence_idle)— a session with a live Remote Control registration is never idle for TTL purposes, regardless of whether a turn is running.CLAUDE_CODE_REMOTE_SEND_KEEPALIVESshould default on for interactive Remote Control sessions. As shipped it is a flag that only takes effect in the state where it is unnecessary.A diagnostic note for anyone landing here from a tmux report
tmux is a red herring, and it is costing people debugging time. I checked v2.1.233 for any tmux-attachment awareness and there is none: zero occurrences of
list-clients,session_attached, orclient_attached; the onlyfocusOutsymbols in the binary come from a bundled web UI library, so terminal focus events are wired to nothing; and the heartbeat is a plainsetTimeoutchain that node runs regardless of whether a terminal is rendering. A detached tmux pane still owns its PTY and nothing is suspended.tmux appears in every one of these reports (#28914 and its duplicates) simply because tmux is how people leave a session idle. On my machine the correlation was perfect and entirely spurious: the one session that dropped was the only one that sat idle, while sibling sessions in the same tmux server, under the same detach events, ran autonomous multi-hour tasks and never dropped once.
Cross-references
Win11/Max20 plan/v.1.30096.1/remote control at startup(true):
Happens to me on Windows11 both with powershell and claude desktop app constantly. Steps to reproduce are to just turn on a /remote-control session and eventually it will disconnect after a few hours (even thought the host computer is on and the session is still there in host computer)...
What's the point of setting up a remote-control if I have to be back at my terminal to turn it on every now and again...
The failure isn't really RC — it's that the session's lifetime is chained to one machine's uptime. If your network/wifi blips, or the host sleeps → the session dies and RC has nothing to reconnect to.
In order to fix this for myself, I had to invert the setup: my claude session runs on a small always-on box (a VPS I own), and every device — laptop, phone — is just a client. I simply use claude desktop/mobile apps and they see the remote claude's session 24/7. Since then "silently died" hasn't happened once, because nothing the session depends on ever sleeps.
Disclosure: I ended up building tooling around this pattern, so I'm biased — but the pattern works with plain tmux + SSH too.
I've noticed now in sessions started directly in windows 11 powershell RC persists... its been at least 1 week and nothing has gone down. but for sessions started on the claude desktop app on windows 11 RC dies after a few hours... the fix is to just not use the desktop app on the actual host that needs to keep RC on... you can still use it the desktop app remotely, that doesn't affect anything
Confirming on v2.1.246, with an environment that rules out sleep and network as contributing causes
Adding a data point that isolates the server-side TTL as sufficient on its own, since an earlier comment suggested the trigger may be network blips or host sleep. On this machine neither can be a factor:
| Factor | Value |
|---|---|
| Host | Mac Studio (always-on desktop, never mobile) |
|
pmset|sleep 0,displaysleep 0,disksleep 0,standby 0— sleep fully disabled || Network | Wired Ethernet (en0), UniFi gateway |
| Link flaps | 0 over a 6-hour window (
log showon link state) || Claude Code | v2.1.246 (thread's most recent confirmation was v2.1.233) |
| Plan | Max |
No sleep. No wifi. No link transitions. Sessions still die on idle. Whatever else may compound it, the server-side TTL reproduces this in a completely static environment.
Scale
11 concurrent named interactive sessions, each launched with
--remote-control <name>andremoteControlAtStartup: true:Attention rotates across them, so most sit idle for long stretches by design. Every one of them produces
could not reach the Remote Control server for about 30 minutesand requires a manual/remote-control.The part that makes this more than a nuisance
Idle is not an edge case for Remote Control. It is the default state of the use case. Nobody enables RC to supervise a session they are sitting in front of; they enable it because they are about to walk away. A TTL that only tolerates presence breaks the single scenario the feature exists to serve.
Concretely: start a long task, leave, have a 30-minute conversation somewhere, pull out your phone to check on it, and the session is unreachable with no way to recover it from the phone. Reconnection requires physical access to the desktop, which is precisely what you did not have. The practical workflow that survives today is "stay at your desk, keep poking the session," which is the opposite of remote control.
That timing is what turns a reconnect bug into a design problem: the failure lands exactly when the user is furthest from the machine and least able to fix it. Worth weighting when this is prioritized against a server-side change.