fix: reconnect faster after remote server updates - #5404
Conversation
A remote update restarts the server for ~15 seconds, but the client shows "Resuming" for up to ~33s and the relay tunnel comes up ~5s behind the HTTP listener. The client forked a fiber that nudged the connection supervisor once, on the first backoff entry. That nudge fires at ~T+2s while the server is still down for another ~11s, so it is wasted, and the supervisor then climbs its 1/2/4/8/16s ladder with attempts landing at ~3, 5, 9, 17 and 33s. Nudge on every backoff entry instead, paced one second apart, so attempts stay ~1s apart for the whole restart window. The fiber stays a child of the update command, so it is interrupted as soon as the update settles. On the server, drop the 250ms sleep in front of the startup cloud link reconcile. Routes are already serving when activation opens that gate, and the retry schedule covers what the sleep hedged against. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| * Callers fork this as a child of the update command so it is interrupted as | ||
| * soon as the update settles, whether it succeeds, fails, or times out. | ||
| */ | ||
| export function nudgeReconnectDuringUpdateRestart(input: { |
There was a problem hiding this comment.
🟠 High state/server.ts:166
The sleep-then-retry in nudgeReconnectDuringUpdateRestart can tear down a connection it just succeeded in establishing. After filtering a backoff event, the function sleeps for ~1s and then fires retryNow unconditionally. If the supervisor reconnects on its own during that sleep, the stale nudge still sends a retry signal, which causes monitorConnectedLease to return false and tear down the new lease — discarding the successful connection before the lifecycle ready event arrives. Consider re-checking the supervisor state (or making retryNow a no-op when not in backoff) before issuing the nudge.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/client-runtime/src/state/server.ts around line 166:
The sleep-then-retry in `nudgeReconnectDuringUpdateRestart` can tear down a connection it just succeeded in establishing. After filtering a `backoff` event, the function sleeps for ~1s and then fires `retryNow` unconditionally. If the supervisor reconnects on its own during that sleep, the stale nudge still sends a retry signal, which causes `monitorConnectedLease` to return `false` and tear down the new lease — discarding the successful connection before the lifecycle `ready` event arrives. Consider re-checking the supervisor state (or making `retryNow` a no-op when not in `backoff`) before issuing the nudge.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4dedb6a. Configure here.
| ), | ||
| Effect.timeoutOption(SERVER_UPDATE_RESUME_TIMEOUT), | ||
| Effect.ignore, | ||
| ); |
There was a problem hiding this comment.
Stale reconnect nudge race
Medium Severity
nudgeReconnectDuringUpdateRestart sleeps on each backoff entry and then always calls retryNow, without checking whether the supervisor is still backing off. retryNow also tears down an active session, so a reconnect that succeeds during that sleep—common when SubscriptionRef.changes joins mid-backoff or the supervisor’s own 1s timer wins the race—gets interrupted and the resume wait stretches again.
Reviewed by Cursor Bugbot for commit 4dedb6a. Configure here.
ApprovabilityVerdict: Needs human review 1 blocking correctness issue found. Two unresolved review comments identify a potential race condition where the new reconnect nudge could tear down a successfully established connection if the supervisor reconnects during the 1-second sleep. This bug concern warrants human review before merging. You can customize Macroscope's approvability policy. Learn more. |


A remote server update restarts the server for about 15 seconds, but the client shows "Resuming" for up to ~33s. Separately, the relay tunnel becomes reachable ~5s after the new server starts listening.
Measured from
boot-service.logacross recent updates: old server dies at T+2s, new server listens at T+7-9s, relay tunnel reachable at T+13-15s.Client: nudge reconnect for the whole restart, not just once
The update flow forked a fiber that waited for the first
backoffstate and calledretryNowonce. That nudge lands at ~T+2s while the server is still down for another ~11s, so it is wasted. The supervisor then climbs its normal1/2/4/8/16sladder and attempts land at ~3, 5, 9, 17 and 33s, which is how a ~15s restart becomes a ~33s wait.Now it nudges on every backoff entry, with a one-second sleep before each one, so attempts stay ~1s apart for the whole restart window. The sleep is the pacer: a connection that fails instantly (ECONNREFUSED, or a 502 through the relay) re-enters backoff immediately and would otherwise spin a tight retry loop.
The fiber stays
Effect.forkChild, so it is interrupted as soon as the update command settles. With that in place the 30s cap is unnecessary and now matchesSERVER_UPDATE_RESUME_TIMEOUT, covering the realistic restart window instead of cutting off at 30s.The loop is extracted as
nudgeReconnectDuringUpdateRestartso it is testable without standing up anAtomRegistry; two tests cover the repeated-nudge count and the pacing.Server: what the ~5s actually is
Worth stating plainly, since it changes what is fixable here: the ~5s between
Listening on http://127.0.0.1:3773andRelay client process startedis not local serialization behind activation. Activation opens ~50ms after the listen log, and nothing is logged in the remaining window. The time is spent inside the two relay calls inreconcileDesiredCloudLinkWith, dominated by server-side Cloudflare control-plane work onPOST /v1/client/environment-links— a serial chain of tunnel create, configure, and DNS mutations. Measured locally: secret store I/O is under 1ms total, the ed25519 keypair is genuinely first-run-only, and network transport for both POSTs is ~0.6s.That chain runs on every boot because
releaseManagedTunnelOnShutdowndeletes the tunnel at shutdown (a deliberate Cloudflare per-tunnel billing tradeoff), so the next startup always pays full tunnel-creation latency instead of reusing an existing tunnel.So moving the reconcile earlier would not have bought the 5 seconds — the work is remote and already starts promptly. Keeping the reconcile gated on activation is also the safe choice: it preserves the finalizer ownership rule at
server.ts:564and keeps a rolling-back trial from fighting the previous version for the tunnel. I left that gate alone and removed the one piece of local dead time on the path, the 250ms sleep in front of the first attempt. Routes are already serving when activation opens the gate, and the retry schedule covers what the sleep hedged against.The real leverage is relay-side (keep the tunnel alive across restarts, or parallelize the independent legs of
provision), which is out of scope here.Validation
packages/client-runtimetests: 524 passedapps/servertests: 1846 passed, 7 skippedNote: running the server suite on a machine with a live T3 server leaks
T3_SERVICE_LAUNCHER_CONTEXTinto the test process and fails 125 tests onmainas well. Unsetting it makes the suite pass on bothmainand this branch.Made by Claude Fable 5 running in Claude Code.
Note
Medium Risk
Touches update/reconnect timing and server cloud-link startup reconcile—user-visible recovery paths but localized changes with new tests on the client nudge logic.
Overview
Shortens "Resuming" after a remote server self-update by keeping client reconnect attempts on a ~1s cadence for the whole restart, and trims a small server startup delay before cloud link reconcile.
Client: Replaces the one-shot "first
backoff→retryNow" fiber withnudgeReconnectDuringUpdateRestart, which reacts to every supervisorbackoffduring the update (1s sleep before each nudge so instant failures do not spin). The nudge fiber still forks as a child of the update command and now times out withSERVER_UPDATE_RESUME_TIMEOUTinstead of 30s. Unit tests cover repeated nudges and pacing.Server: Drops the 250ms delay before the first
reconcileDesiredCloudLinkattempt after activation; exponential retry on reconcile is unchanged.Reviewed by Cursor Bugbot for commit 4dedb6a. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Reconnect faster during remote server updates by retrying once per second across the restart window
nudgeReconnectDuringUpdateRestartin server.ts, which watches connection state changes and triggers a retry for everybackoffphase at ~1s intervals untilSERVER_UPDATE_RESUME_TIMEOUTis reached.reconcileDesiredCloudLinkcall in server.ts, so the initial reconciliation starts immediately.📊 Macroscope summarized 4dedb6a. 2 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.