Repository navigation
Scaling T3 Code to 1+ trillion tokens per month #31002
Replies: 12 comments
Decision 1: forge CLIs get their own process pool; git's pool follows the machineQuestion (two parts, answerable separately).
Evidence. On the same busy server, git commands waited about 11 s (p50) for a slot held by slow forge calls, even with #28431's forge cap. 2 tests fail on If not: (a) alone, without (b). PR: |
Decision 2: turn starts wait while the event loop is saturated, on by defaultQuestion. May the server hold turn-starting effects to half its effect workers while it is saturated, on by default? The setting is
Evidence (A/B on a fork build, median of 3):
If not: the same gate, off by default. PR: |
Decision 3: git commands start from a spawner process, not the serverQuestion. May the server start the VCS layer's git commands from a small long-lived spawner process?
This answers #21289, which was closed pending libuv 1.53's
Overlap. #30209 uses the same mechanism and #30238 touches the same wiring. We're happy to build on whichever you prefer instead of adding a third. Limit. A spawn stuck on a hung mount holds back later commands in the one spawner process, though not the event loop. If not: the same, off by default. PR: |
Decision 4: every short-lived command through the spawner; provider hand-off on LinuxQuestion. Building on decision 3:
Evidence.
If not: short-lived commands only, without the provider hand-off. PR: |
Decision 5: checkpoints read refs and commits in-processQuestion. May checkpoints read refs, HEAD and commit metadata in-process instead of spawning git for them, and cache
Evidence.
Size. About 930 lines of code and 1,900 of tests. That's why we're asking first. If not: keep spawning git; only the cached probes (a separate, no-decision PR after #30838) go in. PR: |
Decision 6: live shell deltas projected once and fanned outQuestion. May the server project each committed batch's shell deltas once for all shell subscribers, using a shared hub with one read per thread per batch? This replaces #29101's per-subscriber batched read, and keeps (and tests) its guarantee. A wide batch reaches each client as one chunk. Evidence (against the same build without it):
If not: keep #29101's per-subscriber read. PR: |
Decision 7: a lagging live stream resyncs instead of endingQuestion. When a shell, thread or guest subscriber's live buffer overflows, may the server resync it from a fresh base snapshot instead of ending the stream with Behaviour change for guests. Link expiry and access checks now run on each live segment, so a resync checks both again. An expired link still ends with Evidence.
Not explained yet. An earlier form showed dispatch p95 +35 % at 250 sessions (399 → 541 ms). We'll re-measure this exact change if you want it. If not: keep ending the stream; clients reconnect. PR: |
Decision 8: a setting for the SQLite reader pool sizeQuestion. May we add
Evidence (fork builds, at 80 sessions):
So it's a setting for large servers, not a new default. 9 of 10 new tests fail on Related. #27979 (open) already does the routing half (a read goes to whichever reader frees first), so this PR is the setting only. If not: the pool stays fixed at 4. PR: |
Decision 9: small SQLite reads no longer queue behind history readsQuestion. May reads be classed as small, page or history, and scheduled by class: work-conserving, with aging so no class starves? Results are unchanged; only the order of reads under contention changes. Evidence.
Overlap. #27979 adds one queue for any free reader. If it lands first, this rebases onto its queue and replaces it with the class queue. If not: no change. PR: |
Decision 10: reader transactions end before their rows are decodedQuestion. May history-sized reads decode their rows after COMMIT, so a reader transaction lasts only as long as the rows phase? They would hold one of Evidence (on
Cost we found. With 3 permits, a burst of 8 history reads runs in three waves instead of two. Now that readers are held only for the rows phase, permits equal to the number of readers may be the better rule; we'd settle it with a benchmark before opening. If not: no change. PR: |
Decision 11: thread shells stop counting every turn item on each readQuestion. May a counts table replace the
Trade-off.
If not: keep counting on read. PR: |
|
Note Written by Claude Opus 5.5 (Claude Code) on behalf of Navid Direction check: the six performance PRs that are already openThese change no behaviour, default or setting, so I opened them directly. CONTRIBUTING asks non-trivial work to agree its direction here first, so: please confirm they're fine to review as they are, or tell me which to hold or split.
Each has before/after evidence in its description. Results are unchanged for clients: the existing suites, integration and replay fixtures pass on each. #30873 overlaps @juliusmarminge's #29521 (the same index, without the |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Human Written
I've been scaling my development pipeline for the past 5 months. It covers development and maintenance of three large monorepos that are in production. My background is development and infrastructure, specifically scaling applications and workloads.
As I've scaled up, I've hit one bottleneck after another: first planning and process, then models, then the harness, then local resources (CPU and RAM), then I/O. As I resolved each one I was able to scale up productively, and I'm now landing hundreds of MRs per day with full e2e testing.
I moved to T3 Code a couple of weeks ago, which let me scale up even further (see October's numbers above), but unfortunately I hit my next bottleneck pretty quickly. I noticed a memory leak on both the release and nightly versions, as well as slow responses once I went past around 75 to 100 active subagents. I confirmed these weren't local resource limits (CPU, RAM, I/O), although technically it was a single-core CPU limit.
Just to note, my workload did make this worse: our repos are 50+ GB each, and I have a Grok approval bot that polls T3 through MCP and answers the prompts that
Full accessstill raises. But even with other workloads you will still hit these limits, maybe just at a higher number, and the improvements save CPU cycles and improve performance across the board.I've spent a few hundred billion frontier tokens (Fable 5.1 planning agents, Opus 5.5 implementation agents, Astra/Sol code review loops until clean, DS 4.1 Flash/GLM 5.3 security loops until clean) to find the current limiting factors in T3 (nightly), and ran multiple optimisation loops, giving each several Linux VMs and Macs for testing.
I've created this discussion to share the results with the community and to propose next steps (full benchmarks and details below).
The main bottleneck, of course, is that the T3 server runs almost all its work on one JavaScript thread, Node's event loop. The team is clearly thinking about this already: SQLite reads run on worker threads, and thread search and compaction discovery recently moved onto them (#19382, #21881). Recent upstream work also cut git processes (#27103, #27980, #28582). But I couldn't find any work moving orchestration, projection or client updates off the main thread. I'd love to take this on if @t3dotgg, @juliusmarminge, @maria-rcks and the team would like a quick discussion on the approach; I feel I have good context on this now, and the setup built to deliver it quickly.
Here's what we discovered and fixed in the PRs:
AI Compiled
Compiled by Claude Opus 5.5 (Claude Code) from the benchmark archive and the review records. Every number comes from the runs in the raw data; failed runs are marked, never hidden.
What was measured
Four builds of the T3 Code server, each run from its single-executable build with only its recommended settings: Upstream (
main@12ec17ce4) and the performance fork's three release candidates, RC1, RC2 and RC3 (RC3 includes12ec17ce4).Results
PRIMARY, synthetic database: turns completed in 5 minutes / turn p50
PRIMARY, sanitised production database: turns completed in 5 minutes
STRESS:
Memory over long runs (sanitised production database, 250 sessions): RC3's peak RSS over 2 h is 3.2–3.3 GiB against 4.5 GiB for RC2. Two 4-hour soaks show a flat heap (+6 to +11 MiB/h, inside the GC sawtooth): no leak signal.
One session (one user, one window, empty database), so a single user isn't worse off:
Startup: RC3's warm start is 1,660–1,673 ms, against upstream's 1,663–1,678 ms.
Every metric at every size, per build (tables)
The full report, with a table under every chart, the per-change ladder and every failed run marked, is
report.mdin the raw data. Per-run CSVs for each profile, the JSON behind the charts and the 1 Hz memory series are in the same gist.Costs, stated plainly
The memory leak
The leak was in
@effect/platform-node4.0.5: a response fiber stayed stuck whenever a client disconnected mid-response, which a polling client (like the Grok approval bot above) does all day. The fork carried a backport; upstream fixed it on 3 November with the Effect 4.1.2 upgrade (#26833). #18485 (nightly reaching about 11.5 GB while agent progress stalls) looks like the same pattern.Where the gains come from
mainfirst:Next steps: moving work off the main thread (a proposal to discuss)
query(), Codex app-server and ACP streams);Pull requests
Open now (each solves one problem, has a test that fails on
mainfirst where there's behaviour to show, and was reviewed until clean):Thanks to @maria-rcks, who has already verified all six of the first PRs (#30818, #30830, #30834, #30836, #30838, #30846) end to end in a real dev environment.
Ready, waiting on a decision below: the forge pool (1), the admission gate (2), the spawner process (3) and spawner for all commands (4), in-process checkpoint refs (5), the shell delta hub (6), resync instead of ending a stream (7), a setting for the reader pool (8), read classes (9), decode after COMMIT (10), and maintained turn-item counts (11).
Ready, waiting on another PR: cached git probes (after #30838) and encode each shell batch once (after decisions 6 and 7).
Decisions we're asking for
Each decision is a comment below, so it can be answered, and its approval linked from exactly one PR. Where we're not sure a change is worth it, we say so.
Raw data: every run as CSV, the JSON behind the charts, the 1 Hz memory series and the full report.
The code in the PRs was written with Claude Opus 5.5 and Claude Fable 5.1 via Claude Code (each PR names its own) and reviewed by GPT-6.1 Sol via Codex until clean.
All reactions