Skip to content

perf: TTFT roadmap after control-plane work — placement, prompt caching, container wake #387

Description

@Yevanchen

Context

The 2026-07-20/21 TTFT effort merged seven control-plane PRs (#369, #379#384). All of them are provider-agnostic: they act before the model call or inside the Sandbox layer (accept path, hydration, dispatch CAS, filesystem RPC concurrency, telemetry deferral, read dedup). After that work, a 32-pair crossover on the two dedicated perf stacks (start 77eb6a84 vs final ba47018b, warm-ping instrument, 64/64 runs correct) shows:

  • End-to-end TTFT paired median +2.28s with bootstrap 95% CI [−1.75s, +6.95s] — crosses zero; no causal end-to-end claim. The two crossover phases disagree in sign (−3.4s vs +8.8s), consistent with every earlier cohort: provider first-token drift (±10s) dominates.
  • Per-cell TTFT medians (cattle cold bring-up): 11.9s (final @ stack-b) / 23.0s (final @ stack-a); start build: 12.4s / 24.5s. The same build differs by ~11s across two identically configured stacks.
  • Control plane is no longer the bottleneck: hydration ~0.2s, real dispatch ~0.1s. Remaining waits: container wake (prepareFilesystem 2.4–4.8s), driver/CLI readiness, provider first token (one ping = 12,413 input tokens with cached_read=0).

Lab artifacts (perf workspace): warm-ping-ab-v26…v29-* JSON series; nightly ledger autoresearch-ledger-20260721.md.

This issue records the three next optimization directions with mechanics and cost estimates.

1. Placement engineering ("stop renting a bad stack")

Same config ≠ same physics. Measured decomposition of the ~11s stack gap:

  • D1 round trips: stack-a consistently ~70ms/read slower (hydration stage delta ~0.69s, driver_turn delta ~40ms over 2 writes) → the two D1 instances were created in different locations.
  • Container node quality: stack-a min 11.3s ≈ stack-b median → a scheduling-luck distribution problem, not a constant.
  • Residual ~7–9s sits in the driver/provider window per stack (suspected slow-node knock-on; not yet isolated).

Plan:

Step Action Effort
Instrument /api/health?deep=1: timed SELECT 1×3 (D1), DO echo, container_api_ready marker ~0.5 d
Attribute Run probes on both stacks; split the 11s into D1 / node / provider buckets ~0.5 d
Fix Recreate D1 with location hints (staging data disposable); DO locationHint; Worker Smart Placement; optionally container instance_type upgrade (unit cost ×2–4, gate on measurement) config-level
Loop Probe readings into observability; alert + recreate on bad placement 0.5–1 d

Total ~2–3 engineer-days plus optional instance-type run cost. Ceiling: bring every stack to today's stack-b level.

2. Provider-layer prompt caching (the only provider-specific work)

Evidence: a ping run pays full prefill for 12,413 input tokens every message (cached_read=0) while the prompt's bulk (system prompt, tool schemas, skills) is byte-identical within a session.

Runtime Mechanism Work Estimate
openai Automatic prefix caching (≥1024 tokens, byte-stable prefix) cached=0 ⇒ the prefix is being broken. Audit prompt assembly in the driver/app-server for volatile content near the top (timestamps, run/session ids, unstable tool ordering); stabilize serialization, move volatile parts to the end 0.5–1 d audit + 0.5 d fix + 0.5 d A/B (driver repo)
anthropic (claude-agent-sdk) Explicit cache_control (reads −90%, writes +25%) Agent SDK likely already enables it — verify cached_read in benchmark usage rows first (unverified) 0.5 d verify, probably zero change
opencode / OpenAI-compatible Same as openai shape Rides along with the openai fix

Expected: cache hits typically remove 1–3s of provider TTFT at this prompt size (to be A/B'd), and cut input cost by ~50% (OpenAI) / ~90% (Anthropic reads) on the cached share. The only lever that reduces latency and the bill at the same time. Total ~2–3 engineer-days, concentrated in the driver repo.

3. Container wake cost (the cost-model front)

prepareFilesystem 2.4–4.8s is the first RPC waking the container (image boot + entrypoint). Cattle policy tears the world down after every terminal run by design (closeOnRunTerminal, idleReleaseDelayMs=0), so every cattle run pays it. Ladder, cheapest first:

Rung Action Cost Expected
a. Pay less often Pet already has a 5-min idle grace; lengthening it is a one-line policy value traded against container-hours. Cattle keeps teardown semantics where isolation demands it config + residency cost 2nd+ message in a session pays zero wake
b. Make each payment cheaper Image diet: 413MB compressed vs <150MB reference — multi-stage build, strip toolchains/caches; measure entrypoint first via container_api_ready 2–4 d (driver repo), no run cost est. −30–50% wake time (to be measured)
c. Pay in advance Warm pool: N pre-booted unassigned containers, bind on demand; moves the 2.4–4.8s to when nobody is waiting ~1–2 wk (pool mgmt, reclaim, leak guards) + N × instance residency wake → sub-second; the decisive step for a stable sub-10s p95

Suggested order: b-measure → a → c (delete and simplify before automating spend).

Sequencing note

Directions 1 and 2 are ~3 engineer-days each with near-zero run cost; direction 3 climbs from free config to real standby spend. All three are additive to the merged provider-agnostic control-plane work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions