« Phase 12 README · Warmup · Deep Dive · Principal Deep Dive · Core Contributor · Staff Notes
Phase 12 — Hitchhiker's Guide
The compressed practitioner tour. Read the WARMUP for the derivations; this is the stuff you say in the meeting.
30-second mental model
An agent in production is an I/O-bound network service — it spends its life waiting on a
generating LLM and tools, using almost no CPU. So you serve it async: thousands of requests
await at once on one event-loop thread (concurrency, not parallelism — never block the loop).
You stream tokens over SSE because perceived latency is time-to-first-token, not total
time. You keep state outside the process (Redis + TTL) because instances are disposable. You
cap concurrency with a semaphore and shed load past the queue to protect a rate-limited
LLM. You make retries safe with an idempotency key. You fan events out with pub/sub. And
you drain in-flight requests on deploy so nobody's stream gets cut. Build all of it from
asyncio; the frameworks are ergonomics over these exact mechanisms.
The numbers to tattoo on your arm
| Number / rule | Meaning |
|---|---|
| agent request ≈ 99% wait, 1% CPU | I/O-bound → async, not more cores |
L = λ·W (Little's Law) | 100 req/s × 8 s = 800 in flight at once on one loop |
| async concurrency: 1 thread, N coroutines | one runs at a time; they interleave at await |
| 1 blocking call freezes the whole loop | no time.sleep/requests.get/CPU loop in a coroutine |
| stream → TTFT ~300 ms vs 6 s total | perceived latency = first token, not last |
SSE frame = event:/data: + blank line | the blank line dispatches the event; end with a done |
Semaphore(max_concurrency) | at most N requests hit the LLM at once; the wait = backpressure |
queue full → fast 503 | shed load; a service that stays up beats one that collapses |
| idempotency key → replay cached, model called 0× | run-exactly-once over at-least-once |
| TTL cache on an injected clock | expiry is exact and testable; never time.time() |
| pub/sub = 1 event → N subscribers | fan-out; a task queue = 1 job → 1 worker |
| graceful drain = stop new, await in-flight, exit | deploys nobody notices |
Framework one-liners
- FastAPI / Starlette — async endpoints;
StreamingResponse(gen, media_type="text/event-stream")streams your async generator as SSE.Dependsfor injection, lifespan for startup/drain. - uvicorn / Gunicorn — the ASGI server; one event loop per worker per core;
--workers Nscales across cores, async scales within a core. - Redis —
SETEX key ttl val/GETis yourTTLCache;PUBLISH/SUBSCRIBEis yourEventBus;SET key val NX EXis the atomic idempotency-key reserve. - asyncio —
gather(run many),Semaphore(cap),Queue(backpressure/fan-out),Event(drain signal),async forover an async generator (streaming). - Serverless (Vercel Fluid / Lambda / Cloud Run) — one instance serves many concurrent invocations because the work is I/O-bound; the platform autoscales on concurrency.
- All of them — sugar over
event loop + await + a semaphore + an external store + a broker.
War stories
- The service that fell over at 3× traffic. No concurrency cap. Every request hit the LLM at
once; the provider returned
429s, retries piled on, the loop drowned. ASemaphore(20)plus a bounded queue and a fast503past it turned a total outage into graceful degradation. - The
time.sleepthat tanked p99 for everyone. One endpoint did a synchronoustime.sleep(retry_after)inside a coroutine "just to back off." It blocked the whole event loop; every concurrent request's latency spiked.await asyncio.sleepfixed it in one line. - The double-charged customer. A mobile client retried a timed-out "resolve + bill" request; no
idempotency key, so the agent ran twice and billed twice. An
Idempotency-Keyheader + a RedisSET NXreserve made the retry a replay. - The deploy that cut off streams. Old instances exited on
SIGTERMimmediately, severing every half-streamed answer mid-sentence. A graceful drain (stop new, await in-flight) made deploys invisible. - The conversation that "randomly forgot" itself. History lived in a local dict; the load balancer sent turn 2 to a different instance. Moving it to Redis with a TTL fixed the "amnesia."
Vocabulary
I/O-bound (waits, doesn't compute) · event loop (one-thread cooperative scheduler) ·
coroutine / await · async generator (async def + yield, the streaming primitive) ·
concurrency ≠ parallelism · block the loop (the cardinal sin) · SSE / EventSource /
text/event-stream · TTFT (time-to-first-token) · semaphore (concurrency cap) ·
backpressure / load-shedding / 503 · idempotency key (dedupe retries) · TTL cache /
SETEX (external state) · stateless service · pub/sub / fan-out / topic / subscriber ·
readiness vs liveness · graceful drain · Little's Law (L = λW).
Beginner mistakes
- Thinking async makes code parallel or faster — it gives I/O concurrency on one thread, nothing for CPU-bound work.
- Blocking the loop with a synchronous call (
requests,time.sleep, a CPU loop) — freezes every concurrent request. - Buffering the whole answer instead of streaming — TTFT = total latency, feels broken.
- Unbounded concurrency — stampedes your rate-limited LLM into
429s; no semaphore, no backpressure. - Accepting all load under a spike instead of shedding it — a slow collapse beats no fast
503. - Keeping conversation/session state in a process dict — lost on the next deploy or a different instance.
- No idempotency key — a retried side-effecting request runs (and bills) twice.
- Reading
time.time()for TTLs/expiry — untestable and non-deterministic; inject the clock. - Exiting immediately on
SIGTERM— severs in-flight streams; drain first. - Confusing pub/sub fan-out with a work queue — duplicates or drops jobs.