Repository navigation
[Bug]: Thread snapshot HTTP handlers have no server-side timeout; cold-open herd times out at 6s and floods relay logs with context-canceled warnings #17765
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- Run the server (official nightly, orchestration V2) against a moderately large
statev2.sqlitewith many threads. - Cold-open a client with many threads mounted and a cold snapshot cache (fresh profile), so every mounted thread misses its snapshot cache.
- Each mounted thread fires
GET /api/orchestration/threads/:threadId/boundedwith the client's fixed 6 s budget (DEFAULT_BOUNDED_THREAD_SNAPSHOT_TIMEOUT_MS = 6_000,packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts:19), with no cross-thread concurrency bound (threads.tsonly has a per-threadapplyLock,packages/client-runtime/src/state/threads.ts:235). - The server serializes all of them on the single-permit SQLite client (
semaphore = yield* Semaphore.make(1),packages/shared/src/nodeSqliteClient.ts:262) underBEGIN IMMEDIATE, each holding the lane across a multi-table projection read insidesql.withTransaction(getThreadSnapshotWindow,apps/server/src/orchestration-v2/ProjectionStore.ts:4856). - The snapshots queue, all exceed 6 s, and every client request fails with
RemoteEnvironmentAuthTimeoutError→unavailable→ socket full-snapshot fallback, while the server-side fibers keep running until the client cancels them. cloudflared logs both sides asIncoming request ended abruptly: context canceled.
Expected behavior
Snapshot HTTP handlers bound their work server-side (timeout with a retryable busy signal the client can back off on instead of hanging until cancel), the client limits in-flight snapshot loads, and downstream-client-goes-away relay lines log at debug rather than warning.
Actual behavior
- The three thread-snapshot handlers (
threadSnapshot,threadBoundedSnapshot,threadHistoryPageinapps/server/src/orchestration-v2/http.ts) have no server-side timeout/deadline — verified by reading the handlers, theThreadManagementService→Orchestrator→ProjectionStoredelegation chain, andtraceLocalHandlerWork(tracing only). A slow read holds its SQLite transaction open until the client gives up and cancels. - The client fans out one 6 s bounded request per mounted cache-miss thread with no semaphore, staggering, or batching, so a shell restore is a thundering herd against the single SQLite lane; mass timeouts then stampede onto the socket full-snapshot path the bounded route exists to avoid.
classifyRelayClientOutput(apps/server/src/cloud/ManagedEndpointRuntime.ts:81) maps the resulting... ERR Incoming request ended abruptly: context canceled ...lines towarningvia the genericERRrule, with no special case (repo-wide grep for the substring: zero hits). Each line becomes aRelay client reported a transport warninglog; nothing actionable —warningon this path only logs (observeConnectorOutput, no restart/recovery). In reporter logs this is on the order of ~135 such warnings/day plus repeated enrichment warns for deleted checkouts on wake.
Impact
Major degradation or frequent failure
Version or commit
Local checkout 0678e4e23d (2026-10-07); re-verified each claim against upstream main tip via the contents API on 2026-10-10 (handler file unchanged — no timeout combinator; classifier unchanged; client still 6 s default with no semaphore).
Environment
Linux server, official nightly builds (no fork). Client: web.
Logs or stack traces
Representative relay lines (from cloudflared output observed in reporter logs):
ERR Incoming request ended abruptly: context canceled ...
Server side, each corresponds to a snapshot handler fiber dying on Interrupt after the client's 6 s cancel; client side, RemoteEnvironmentAuthTimeoutError → unavailable → socket fallback.
Workaround
None on official nightlies — snapshot timeouts/knobs are not exposed via config or environment (no *_TIMEOUT/snapshot_* env knobs found in packages/ or apps/), and the relay log level for the spawned cloudflared process is fixed at --loglevel info (ManagedEndpointRuntime.ts:350).
Suggested fix shape (for triage)
- Bound the three snapshot handlers server-side (e.g.
Effect.timeoutOption≈ 5 s, inside the client's 6 s budget) and answer a retryable busy signal (HTTP 429) instead of hanging until cancel. - Bound in-flight client snapshot loads (e.g. a small semaphore) and retry once on busy rather than falling straight back to the socket full snapshot.
- Special-case
Incoming request ended abruptly: context canceledtodebuginclassifyRelayClientOutput— it means the downstream client went away, not a tunnel fault.
Related
- [Bug]: Server: the thread list read blocks the event loop and every other database call for seconds on large databases #14701 covers the sibling shell-snapshot stall (event-loop blocking on large DBs); this issue is the thread-snapshot timeout/herd/log-noise chain. The two compound: any slow snapshot burns the single lane for everyone behind it.
- Already landed upstream (not duplicates): bounded snapshots (feat(orchestrator): introduce new orchestrator #2829), FTL/PNC relay classification (fix(server): surface cloudflared FTL/PNC relay logs as warnings, not debug #5076), tunnel recovery (fix(server): recover a deleted tunnel when Cloudflare says "Tunnel not found" #16648).
Activity
josephv123 commentedon Oct 15, 2026
Consolidated independent reproduction of the shared-writer part of this issue. Verified source at
e536bfd33bc3c351997141d93790df75eeded7b9; the relevant projection/SQLite files are unchanged from the original audit atec1313c8f8333292506f05a355bb72d308a455d8.A tiny synthetic WAL database with one thread and one completed tool item is enough: a worker connection takes
BEGIN IMMEDIATE, signals that it holds the lock, and releases it after 3,000 ms. The main process then schedules a 1 ms heartbeat and calls the productiongetThreadSnapshotWindowmethod. The control wraps that exact method in T3's existingNodeSqliteClient.readOnlyhelper. No provider, relay, real history or large fixture is needed.Retained Node 24.21.0, Linux, alternating two rounds:
Re-running the same portable synthetic probe during consolidation under Node 26.10.0 gave 3,047.3 ms versus 1.33 ms heartbeat delay. Both returned the same one-item snapshot. The first reader use includes worker startup.
The causal path is:
getThreadSnapshotWindowusessql.withTransactionwithoutreadOnly.NodeSqliteClientroutes unmarked work to the synchronous writer, whose transaction usesBEGIN IMMEDIATE.Sqlite.tssets a 5,000 ms busy timeout. The native synchronous wait prevents an Effect timeout or unrelated JS callback from running until it returns.Minimal experimental sequence for a checkout-level regression test:
The complete checkout probe is included below. The worker hold is an experimental input, not a measured production lock frequency. No multi-second stall appeared in the retained production trace windows, so this is a controlled root-cause reproduction, not a claim that every slow cold-open has this cause. No live database was modified.
Complete checkout-level reproduction (temporary synthetic database only)
Install this checkout's normal development dependencies, save the following as
snapshot-contention.mjs, and run from the repository root with Node 24:The script imports production services from the current working directory. It uses a worker to release the experimental write lock independently of the blocked main loop and removes its synthetic fixture on completion. Do not substitute a live database path for
synthetic.juliusmarminge commentedon Oct 21, 2026
Tested current main 70e5e384da on Linux, Node 24.21.0, Chrome web via
vp run dev, using isolated state. An existing populated stopped thread opened successfully. A larger synthetic projection fixture (100 stopped threads × 300 completed command items, 3 KB output/item; 125 MB SQLite file) also opened successfully in a fresh incognito profile. The sidebar contained 100 fixture rows, but opening it requested the selected conversation's snapshot, rather than a snapshot for every sidebar row.A browser-issued burst of 100 distinct bounded-snapshot HTTP requests with 6,000 ms abort deadlines returned 100 HTTP 200 responses in 145–1,513 ms. Opening a populated fixture through the UI returned HTTP 200 in 23 ms server-side:
The controlled writer-lock probe in the existing comment is separate evidence; these results do not disprove contention under that condition. The cloudflared/relay path was not verified.
Please provide the current affected build, approximate database size/thread/item counts, the exact UI steps that mount multiple conversations during a cold open, and sanitized request timings showing the bounded requests timing out and falling back. Those details would distinguish the cold-open trigger from the independently demonstrated injected-lock stall. No database contents or credentials are needed.