Systems

Six writes to one: what happened to our first token

We rebuilt the path from the send button to the first model request around a single owner and a single commit. Commits dropped by up to 45%. Latency did not move. Here is the trace, and what it taught us.

Author
Engine teamSystems engineering
Published
Reading time
16 min
TL;DR
  • A warm message to a coding agent used to pass through about six notifying Postgres writes and three network hops before the turn started.
  • We gave every thread one owner host that is the only writer of an ordered, epoch-fenced log. A warm send is now one commit before the turn starts.
  • Commits per turn fell 38% to 45%. Time to the first model request did not: the cost had moved into context capture and request preparation.

Where the time went

Time to first token is the latency people feel. The model’s own time to first token is mostly out of our hands, so we care about everything before it: from the moment our server receives the message to the moment the request leaves for the model provider.

For our coding agent, that path ran through three services and Postgres. In a development build we measured 86 ms to turn/started and 133 ms to the first model request. Not terrible. But the trace showed something worse than the number: the path was doing a lot of coordination work that no one had asked for.

Anatomy of a send

One warm user message went like this. The engine authorized the request (two reads) and enqueued it (a transaction plus a NOTIFY). It authorized again, extended a grant, and called the harness host’s Wake. The host claimed its mail (three statements), claimed the queue item (another NOTIFY), started the turn, recorded the input (NOTIFY) and recorded the turn start (NOTIFY). Only then did it publish turn/started.

Every notifying write made every engine re-read the thread and push an update to every viewer. Per send, that was six info updates and three queue re-lists in each browser. Toggle between the two runs below.

Fig. 1 · interactive
100%
Warm send → first model request

One warm turn, local Docker. Hover or focus a span for its timing.

0 ms20 ms40 ms60 ms80 ms100 ms120 ms140 msengineauthorize (2 reads)postgresenqueueengineauthorize again, extend grantharness hostWakepostgresclaim mail ×3postgresclaim queue itemharness hoststart turnpostgresrecord inputpostgresrecord turn startviewers6 info re-reads, 3 queue re-listsharness hostcontext, prepare first requestturn/started 86 msfirst model request 133 msOrder of operations from the code path; span widths are schematic. Endpoints (86 ms, 133 ms) were measured.
NOTIFY → every engine re-reads the threadNetwork hopMeasured medianSchematic width
A warm send, before and after. Before: the order of operations, with measured endpoints. After: medians of measured spans. Overlay shows where the time moved.
Fig. 1A warm send, before and after. Before: the order of operations, with measured endpoints. After: medians of measured spans. Overlay shows where the time moved.

One owner, one log

Removing hops one at a time would have helped a little, and kept the shape that caused them: three parties writing the same thread’s state and using Postgres notifications to tell each other. We changed the shape instead.

Every thread gets one owner host, and that host is the only writer of the thread’s durable state. The state is an ordered log in Postgres. Its projections (queue, status, turns, unread counts) commit in the same transaction as each entry. The engine becomes a stateless edge: it authorizes once, looks up the owner, routes the command and fans the owner’s stream out to viewers.

Fig. 2
100%
BrowserwebsocketviewersEnginestateless edgeauthorize oncelook up owner, routefan out owner streamOwner host · one per threadcoordinator(thread)the only decision makerwriter(thread)the only log writer, orderedCodexThreadManagerembeddedowner streamevents +control statePostgresharness_thread_log+ queue+ status+ turns+ unreadsame transactionAppendfan-outcommandlive state pushed to viewers · no re-reads, no NOTIFY on the hot pathfence: (incarnation, epoch)
Three roles, one rule each. The engine routes, the owner host decides and writes, Postgres stores and fences. Nothing on the hot path waits on NOTIFY.
Fig. 2Three roles, one rule each. The engine routes, the owner host decides and writes, Postgres stores and fences. Nothing on the hot path waits on NOTIFY.

A single writer is only safe if a stale writer cannot write. Each Append runs inside a transaction that locks the thread’s control row and checks a fence: the owner’s incarnation and epoch. Assigning a thread to a new host bumps the epoch, so an old owner that wakes up after a pause fails its next write instead of corrupting the log.

append fence (simplified)sql
SELECT owner_incarnation, owner_epoch
  FROM harness_thread_control
 WHERE thread_id = $1 FOR UPDATE;

-- assigned: must match (incarnation, epoch) + live heartbeat
-- unassigned: incarnation IS NULL AND epoch = expected
INSERT INTO harness_thread_log (...) VALUES (...);
-- projections: queue, status, turns, unread (same transaction)
COMMIT;
Trade-off A thread now has one point of coordination, so a host failure means a hand-off: a new owner takes the epoch and continues from committed history. We accepted that cost for strong per-thread consistency without a separate coordination service.

Measuring it

We measured the final build with live traces: seven turns on a long existing conversation, one cold, four warm short answers and two warm turns with three sequential shell commands each. That gives local medians, not production percentiles, and the “before” column is an earlier observation rather than a controlled A/B test. With those caveats, the database side moved clearly.

−38%commits for a warm short answer (8 → 5)
−45%commits for a three-tool turn (20 → 11)
117 msmedian warm receipt → model request (goal: < 40 ms)
Fig. 3 · interactive
100%
Committed append batches per turn

Distinct Postgres transaction ids, submission → finalization · local Docker

05101520Short warm answerThree sequential shell calls
BeforeAfter
Committed append batches per turn, counted as distinct Postgres transaction ids from submission to finalization. Row counts did not fall; transactions did.
Fig. 3Committed append batches per turn, counted as distinct Postgres transaction ids from submission to finalization. Row counts did not fall; transactions did.

The latency did not follow. Socket receipt to the gateway request was about 100 ms before and a median of 117 ms after, within noise of each other. The table shows why: the single append is cheap, but the work around it is not.

Boundary (warm)BeforeAfter, median
Socket receipt → gateway request~100 ms117 ms
Authorization9 ms10.5 ms
Submission and dispatch append27 ms25.6 ms
Initial step-context capture27–33 ms28.7 ms
Tool completion → next requestn/a34.9 ms
Stream end → completion published81–92 ms80.0 ms

What remains is cold router construction, repeated environment reads and context capture on every step. None of these are coordination; they are just work. Removing the coordination made them visible.

What we did not do We did not turn off synchronous commits for history, and we do not publish a completion before it is durable. Either would make the interface claim more progress than the database can prove. We also did not add router or prompt caches without a profile that justifies their invalidation cost.

The gateway hop

The last stop before the provider is our model gateway. Every model call that leaves Langdock passes through one Go service that embeds the Bifrost library for provider protocols. Around it we add caller identity, tenant authorization served from caches, a fail-closed capability policy and admission control.

Fig. 4
100%
callerModel gateway (Go)identitywho is callingauthorizecached decisionscapability policyfail closedadmission1 Redis round tripBifrostprovider APIsproviderreserve atomically: tokens, concurrency,provider capacity, budget headroomusage evidence → accountingsettles later, off the request path
One gateway for every model call. Admission reserves tokens, concurrency, provider capacity and budget headroom in a single round trip; usage is settled afterwards, off the request path.
Fig. 4One gateway for every model call. Admission reserves tokens, concurrency, provider capacity and budget headroom in a single round trip; usage is settled afterwards, off the request path.

Admission is where latency and correctness meet. A spend check that reads counters updated after usage lets concurrent requests all pass. The gateway reserves capacity atomically in one Redis round trip instead, and falls back to a bounded local limiter if Redis is down. Billing moves off the hot path: the gateway emits usage evidence and a separate accounting service settles it later.

What we keep

  • One writer per thread. Fewer moving parts, fewer transactions, and a clear answer to “who may change this”.
  • State in the notification. Viewers get at most two control notifications per send, each carrying the state it announces. No re-reads.
  • Connection reuse. Reusing HTTP transports avoided repeated root-store and connection setup, a measured win.
  • State the workload. Every number in this post comes with its build, topology and sample size. A latency claim without them is a guess.

Next up are release-build measurements in our production topology, and Postgres commit load under concurrency. If you enjoy this kind of trace-reading, we will publish those numbers here too.

Want to work on problems like this? We’re hiring