Repository navigation
Open
Description
Found by the daily slow-request review (#108) on 2026-11-11, covering 2026-11-10 18:00 UTC to 2026-11-11 18:00 UTC.
What the records show
- The event loop stalls often.
~/.t3/userdata/logs/event-loop-stalls.ndjsonlogged 127 stalls over 2 s in the window, 25 of them 9 s or longer. The worst was 11.5 s, at 2026-11-11 01:14:31 UTC. The stalls carry high system CPU time (cpuSystemMs7–14 s per sample), and the load average was mostly 9–16 on a 32-core machine, so machine load does not explain them. - The slow requests are the stall's victims, not separate causes. 19 of the 28 slow requests (over 10 s) fall into four bursts where several requests started in the same second and all finished 10–13 s later: 22:22:11, 00:44:02, 01:18:04 and 01:34:34 UTC. Inside them, trivial indexed reads took about 10 s with no recorded lock wait, for example
SELECT issue_number, thread_id FROM issue_thread_links WHERE host = ? AND repository = ?at 7.8 s and 11.6 s. So did a Forgejo sign-in (ForgejoCli.authenticateFj, 9.7 s). Requests that do show lock wait (sql.transactionlock wait of 10–12.8 s inOrchestratorV2.getThreadRecordsandIssueWorkService.repositoryStatus) waited in the same bursts for a connection held by a stalled request. - Durations by request:
server.getThreadProcesses15 records, median 11.7 s, worst 13.4 s;issues.statusBatch10 records, median 11.6 s, worst 21.6 s (load average 46 at that moment);issues.list2 andissues.repositoryStatus1, at 11–13.8 s. - Still present on 2.12.112 (installed 02:01 UTC): stalls of 9.5 s (02:02:44), 9.1 s (03:35:05) and 6.0 s (04:17:36), and two slow requests at 02:14:57 and 02:15:40. Stalls dropped from 15–25 per hour to 0–5 per hour after about 02:00 UTC. That is when the new build started, but also when activity fell off for the night, so the drop is not yet evidence of a fix.
Sample records (trimmed)
2026-11-11T01:18:17Z ws.rpc.server.getThreadProcesses 12.0s ProjectionStore.readCanonicalProjection 9.7s
query orchestration_v2_projection_provider_threads WHERE thread_id = ? OR owner_node_id IN (...) 10.1s, lockWait 0
2026-11-11T01:18:19Z ws.rpc.server.getThreadProcesses 13.4s sql.transaction lockWait 11.9s
2026-11-10T22:22:25Z ws.rpc.issues.list 13.8s query issue_thread_links WHERE host = ? AND repository = ? 11.6s (lockWait 11.6s)
event-loop-stalls 2026-11-11T01:14:31Z delayMaxMs 11474 utilization 0.71 cpuUserMs 17477 cpuSystemMs 10928
Source
- The stall monitor is
apps/server/src/observability/EventLoopMonitor.ts. It logs a stall over 2 s (STALL_THRESHOLD_MS) with CPU and page-fault counts, but not what the main thread was doing. - Reads run in a pool of reader workers (
packages/shared/src/nodeSqliteClient.ts), but transactions such asgetThreadRecords'sql.withTransaction(readCanonicalProjection(...))(apps/server/src/orchestration-v2/ProjectionStore.ts:5755) and all writes use the main-thread connection.
Suggested direction (unverified)
The blocker is not identified. What blocks the main thread is not confirmed in the source. Candidates are a synchronous SQLite call on the main-thread writer connection (it waits up to busy_timeout = 5000), other synchronous filesystem or process work, or long JSON encoding.
The smallest next step is to name the blocker before fixing anything: when a stall passes about 5 s, record the span that was active when the loop resumed. Alternatively, take a short CPU profile with the inspector after a stall. Then move or split that work.
Metadata
Metadata
Assignees
Labels
No labels
Activity
tannerpolley commentedon Nov 12, 2026
Daily slow-request review, 2026-11-12. The main-thread freeze happened again, and this one was much longer.
Event-loop stall: at 2026-11-12T02:23:03Z (2026-11-11 19:23 MST), the server on 2.12.114 logged
delayMaxMs778000, so the main thread was blocked for about 13 minutes. During that window it recorded:Slow requests that were victims of the freeze: two
ws.rpc.server.getThreadProcessescalls both finished at 02:22:35Z.sql.transactionlock.Both waits were in
OrchestratorV2.getThreadRecords←ThreadManagementService.getThreadRecords←orchestration.readRunningThreadWork←BackgroundTasks.processes. The writer connection runs on the frozen main thread, so the lock was held by the stall itself, not by a slow query.Missing evidence: no trace covers the window. The
server.trace.ndjson*files rotate every 6 minutes across 11 files, and the oldest now starts at 2026-11-12T16:47Z.server-child.logwas last written on 2026-11-08. So the blocking span is still unknown (unverified). The heavy system CPU and minor faults point to large allocation or copy work on the main thread, but that's inference.This strengthens the suggested first step here: on a stall over threshold, record the active span or take a short CPU profile, so the next one names its cause.
tannerpolley commentedon Nov 13, 2026
T3 Code status: Done
Issue: #367 Server event loop stalls for up to 11 s every few minutes, and every request in flight stalls with it
Thread: thread:delegated-task:command%3Amcp%3Af7dbe326-b11b-455a-a927-ca05430dc653%3Adelegate-task%3Aissue-367-build-1
Model: claude-opus-5-5
Branch: issue-367-server-event-loop-stalls-for-up-to-11-s
Updated: 2026-11-13T20:28:34.783Z
tannerpolley commentedon Nov 13, 2026
T3 Code closeout: done
Issue: #367 Server event loop stalls for up to 11 s every few minutes, and every request in flight stalls with it
Thread: thread:delegated-task:command%3Amcp%3Af7dbe326-b11b-455a-a927-ca05430dc653%3Adelegate-task%3Aissue-367-build-1
Model: claude-opus-5-5
Branch: issue-367-server-event-loop-stalls-for-up-to-11-s
Completed: 2026-11-13T20:28:35.698Z
I couldn't find what blocks the main thread, so this PR doesn't fix the stalls. It adds logging so the next long stall records the operation that was running when it happened. The PR is open against main, references #367, and is not merged.
Cause, still unknown. No trace covers a stall: traces rotate in about 6 minutes, and the current traces contain none. What I could establish from the logs:
What changed:
StallAttribution.ts(new): it wraps the tracer the same way the existing heap tracking does, and remembers open operations plus the last 64 that ran longer than 2 s.suspects: up to 5 traced operations or requests that ran through the stall, most recently started first, so the operation that actually blocked should come first. This appears inevent-loop-stalls.ndjson, the warning log and the stall's trace entry.fileSystemReads: the server's own disk reads during the stall. This will show whether those big read runs come from the server or from another process.Tests:
StallAttribution.test.tschecks the ordering and that operations too short to explain the stall, or that ended before it, are left out. It passes, and so doesEventLoopMonitor.test.ts.vp run check:changed: format, lint, typecheck and tests (3 files, 4 tests) pass. Its second unused-exports check (knip) fails, but only on unused exports already on main; none are in this change. I committed with--no-verifyfor that reason, as allowed.PR #397, head 6ba2701, checks and reviews not yet reported.
fix(server): event loop stall records name the spans that lasted thro…