TAI BUI
Back to curriculum
BuildPython

Production Scaling — Queues, Checkpoints, Durability

Scaling multi-agent systems to thousands of concurrent runs takes **durable execution**. LangGraph's runtime writes a checkpoint after each super-step, keyed by `thread_id` (Postgres by default). When a worker crashes, its lease is released and another worker resumes. Agents can sleep for as long as they need while they wait on a human. **MegaAgent** (arXiv:2408.09955) ran a producer-consumer queue per agent with three states (Idle / Processing / Response) and two layers of coordination: intra-group chat and an inter-group admin chat. For LLM streaming, **fibers/async** beat thread-per-job, because a thread spends about 99% of its life idle waiting for tokens while a fiber yields on I/O. Ashpreet Bedi's "Scaling Agentic Software" makes the counterpoint: run **FastAPI + Postgres and nothing else** until load proves otherwise. Simple architectures go further than you'd expect. This lesson builds a durable checkpoint log, a per-agent queue with state transitions, and an async-versus-threads demo, then lands on the "start simple" rule.

TIME: ~75 minutesPREREQUISITES: Phase 16 · 09 (Parallel Swarm Networks), Phase 16 · 13 (Shared Memory)

Learning Objectives

  • Name the failure modes that push a multi-agent system from an in-memory loop to durable execution, and the three requirements that make resume safe.
  • Run the checkpoint demo in main.py and explain why worker 2 resumes at super_step 3 with counter 3 after worker 1 crashes.
  • Describe MegaAgent's Idle / Processing / Response queue and its two coordination layers.
  • Explain why async beats thread-per-job for LLM calls, and what the toy benchmark does and does not measure.
  • Choose between FastAPI + Postgres and a durable-execution engine for a given load, and design exactly-once effects with a dedup key and an outbox.

Problem

A prototype multi-agent system runs fine on one laptop with three agents in an in-memory event loop. Then you move to production:

  • Agents sometimes run for hours (long research, human-in-the-loop waits).
  • Worker processes crash, and a restart loses state.
  • Peak load is 10x average, so you need horizontal scaling.
  • Users pay per agent-run, so charging has to be exactly-once.

An in-memory event loop handles none of this. You need a durable execution layer underneath. The 2026 options are:

  1. A workflow engine with checkpoints (Temporal, LangGraph runtime).
  2. A message queue with a state store (Postgres + SQS/RabbitMQ).
  3. Actor-model frameworks (MegaAgent's producer-consumer per agent).
  4. Hand-rolled FastAPI + Postgres (Bedi's argument).

The code in this lesson covers the core of the first option (a checkpoint log with crash resume) and the shape of the third (a per-agent state machine). It also measures async against threads. The other options we compare on paper.

Warm-up0/3
1. Several agents in a swarm update the same record in a shared memory store at the same time. Which guard keeps one agent from silently overwriting another's change?Warm-up
Agent A and agent B both read version 5 of a shared balance record at the same time. A card shows agent A wants to write balance 120 based on that version, and another shows agent B wants to write balance 90 based on the same version, with a question mark over which write succeeds.
2. A long job keeps all of its progress only in a running process's memory, and the process is killed halfway through. What happens to that progress?Warm-up
A process box holding a counter variable in memory, an arrow labelled kill dash 9 4821 pointing down at it, and a question mark over what happens to the counter afterward.
3. A client calls a payment API and the request times out, so the client sends the same request again. What is the main danger?Warm-up
A client sends a charge request for forty two dollars and gets a timeout with no reply. The client then sends the identical request again, with a question mark over whether the first request already ran on the server.

Concept

Durable execution, the pattern

A durable-execution engine persists the full program state after each "step" (a super-step, in LangGraph's language). When a worker crashes:

worker crashes mid-step
  -> lease timeout
  -> another worker picks up the thread_id
  -> resumes from last checkpoint
  -> no duplicate side effects

Three requirements make this work:

  • Serializable state. Everything the agent knows has to be persistable. A closure holding a live database connection does not survive a restart.
  • Deterministic resume. Given the same state and inputs, the agent produces the same actions, or defers to an external deterministic oracle for LLM calls.
  • Idempotent side effects. External calls (tool calls, payments) must be idempotent or carry a deduplication key.

LangGraph writes a checkpoint after each super-step, Temporal writes after each activity, and Restate uses event-sourced journals. All three implement the same pattern.

LangGraph's runtime

Each agent has a thread_id, state is a typed dict, and each super-step writes a row to the checkpoints table. On resume, the runtime replays from the last checkpoint instead of from scratch. An agent can call interrupt() to wait for human input: the runtime persists the state and releases the worker, and when input arrives, any worker can pick the thread back up.

This is the reference production design as of April 2026.

MegaAgent's per-agent queue

arXiv:2408.09955 describes a scale experiment with thousands of concurrent agents in one cluster. The architecture:

agent i:
  state ∈ {Idle, Processing, Response}
  in_queue   <- messages addressed to agent i
  out_queue  -> replies + side effects

coordinators:
  intra-group chat  (agents in the same group)
  inter-group admin chat  (high-level routing)

Two layers of coordination let conversation inside a group stay dense while traffic between groups stays sparse. That split is how the system keeps cost roughly linear as agent count grows.

Async vs thread-per-job

LLM calls are I/O-bound. A thread waiting on the next token is idle about 99% of the time, and threads cost ~1MB of RAM each, so 10,000 concurrent calls means 10GB of stacks before any work happens.

Fibers (Python asyncio, Go goroutines, Rust tokio) yield cooperatively on I/O, so the same 10,000 calls fit comfortably in one process. At LLM-agent scale, async is the architecture itself, not a tuning step.

CPU-bound post-processing (embedding, tokenizer tricks) still wants threads or processes. Keep the I/O layer separate from the CPU layer.

Bedi's counterpoint

"Scaling Agentic Software" (Ashpreet Bedi, 2026) argues that most teams over-engineer before they have measured load. His default:

  • FastAPI + Postgres.
  • Each agent run is a row, updated in place with optimistic concurrency.
  • Background jobs via pg_notify or a simple Celery worker.
  • Retry policy in application code.

For loads under roughly 100 concurrent agent-runs on manageable tasks, that is often enough. Upgrade when you measure it failing.

Adopt a durable-execution framework when you hit a concrete problem a simple architecture can't solve. Adopting one earlier costs you ceremony that doesn't pay off yet.

Exactly-once semantics

For paid agent runs you want "exactly-once effective", which means at-least-once delivery plus an idempotent consumer. Three moves get you there:

  • Dedup key per run. Include it in every side-effect call.
  • Outbox pattern. Side effects are written to a table first, and a separate process executes them. Both steps are idempotent.
  • Compensating transactions. When a side effect succeeds but the write that tracks it fails, schedule a compensation.

These are database-engineering patterns, not LLM-specific ones. The only LLM tax is that the calls are slow. The rest is standard distributed systems.

Rainbow deployment

Anthropic's multi-agent research system uses "rainbow deployments": several versions of the agent runtime run concurrently, so long-running agents don't get killed on every code deploy. You canary a new version on a slice of traffic and retire the old version once its agents finish.

This is standard for long-running stateful systems. What changes in 2026 is that agents can live for hours, so the deploy cycle has to wait for them.

The production checklist

  • Durable state (checkpoints, snapshots, or outbox + replayable log).
  • Idempotent side effects.
  • Async I/O layer for LLM calls.
  • At-least-once delivery with dedup.
  • Rainbow/canary deployment for stateful workloads.
  • Observability: per-agent traces, super-step audit, retry counter.
Check: Concept0/26
1. What is durable execution?Define
Run run-88 moves through step 7, step 8 and step 9, each followed by a saved checkpoint, then the worker dies. A new worker loads the step 9 checkpoint and continues from step 10.
2. In LangGraph's vocabulary, what is a super-step?Define
A timeline of four checkpoint markers in a row. The segment between the second and third marker is bracketed below with a question mark asking what that segment is called.
3. Which key ties together every checkpoint of one agent run, so that another worker can find the run and resume it?Define
A checkpoints table with columns thread_id, super_step and state, holding two rows for thread_id run-A and one row for thread_id run-B. A worker looks up the latest row where thread_id equals run-A, and the two run-A rows are highlighted.
4. What is a checkpoint in a durable-execution system?Define
A single saved snapshot card with a thread id, a super-step number, and a state object holding a counter and a phase. An arrow points to a faint box asking what the next step will be, with a note that a worker reads this snapshot later to continue the run.
5. What does it mean for a side effect to be idempotent?Define
Two cards showing the same instruction, set balance equal to 100, run once and then run again as a repeat, both ending with balance at 100.
6. What is the outbox pattern?Define
An app writes a row for a send welcome email action into a tasks table marked pending. A note says a separate background worker reads that row later, performs the send, then flips its status to done.
7. What does at-least-once delivery guarantee, and what does it cost the consumer?Define
A queue delivering msg-58, about updating an inventory count, to a consumer at time 1, with a note that the acknowledgement was lost, then delivering the same message again at time 4.
8. What is a rainbow deploy?Define
Two runtime version boxes, v1.2 and v1.3, both shown active at the same moment, each currently serving a different agent run.
9. What is an async fiber, as used for LLM calls?Define
One process box containing an event loop and four small tasks, each paused and waiting for a network reply, each connected to the loop by a dashed line.
10. Which set of states does MegaAgent (arXiv:2408.09955) give each agent in its producer-consumer queue?Define
Three boxes in a cycle, labelled only state 1, state 2 and state 3 with arrows forming a loop, next to an in-queue box holding two waiting items and an out-queue box holding none.
11. Which three requirements must hold for a crashed run to resume correctly under the durable-execution pattern?Define
A resumed run passing through three unlabelled gate boxes in a row before it reaches a box marked continues safely.
12. Why does a closure holding a live database connection break durable execution?Explain
An agent state card with a counter field and a live database connection field, an arrow to checkpoint dot write of state, and a question mark over whether the connection field can be written as JSON.
13. Why does at-least-once delivery combined with an idempotent consumer count as exactly-once effective?Explain
A queue delivering msg-91, apply discount code SAVE10, to a consumer at time 1 and again, marked a duplicate, at time 3, with a question mark over the net effect on the order.
14. Why does Bedi advise against adopting a durable-execution framework before there is a measured failure?Explain
A card for the team's current FastAPI and Postgres system, each run a row, next to a card for adopting a durable-execution engine now, noting the cost of a new service and new operations over weeks, with a question mark over whether any failure has been measured yet.
15. Why do fibers suit LLM streaming workloads better than thread-per-job?Explain
A timeline bar for one thread making one LLM call, almost entirely taken up by waiting for the API response, with only a thin sliver at the end marked local work.
16. Why does the lesson keep embedding and tokenizer work out of the async I/O layer?Explain
A pipeline where awaiting the model response leads to two local computation steps, tokenizing the result and computing an embedding, neither one marked as waiting on anything external.
17. How does LangGraph let an agent wait on a human without tying up a worker?Explain
A flow where an agent at step N calls interrupt, its state is saved and the worker returns to the pool, then after a gap marked time passes, a human submits input and any free worker resumes the thread.
18. What are the two coordination layers in MegaAgent, and why do they keep cost roughly linear?Explain
Two clusters of three agents, each cluster with many lines connecting its own members, linked to each other through a single admin node with one line to each cluster.
19. Why does Anthropic's multi-agent research system use rainbow deployments?Explain
A card for Anthropic's research system showing agent run 4471 researching for three hours and forty minutes and not finished, next to a card saying a new runtime version is ready to deploy, with a question mark over what happens to that run.
20. Each of 10,000 concurrent LLM calls gets its own thread at about 1MB of stack. How much RAM goes to stacks before any work happens?Apply
A card for ten thousand concurrent calls next to a card for about one megabyte of stack per thread, with a question mark over the total RAM that goes to stacks before any work happens.
21. After a crash, the resumed run sends the customer's confirmation email a second time. Which requirement was missing?Analyze
Worker A runs step 9, sends a confirmation email, then crashes. Worker B resumes at step 9 and sends the same confirmation email again, leaving two emails in the customer's inbox.
22. A paid run charges a card through an external API. A worker crashes after the charge succeeds but before the run's row is updated, and a replacement worker retries. Which ingredient stops a double charge?Analyze
Worker A calls the payment API, the charge succeeds, and worker A crashes before marking its own row charged. Worker B, a replacement, sees the row still unmarked and is about to retry the charge call, with a question mark over what stops a double charge.
23. Agents wait hours for human approval, and the worker pool runs out of workers even though the waiting agents do no work. What should change?Analyze
Five worker boxes: four marked blocked and one marked free, with a note that the blocked workers are waiting hours on a human approval. A question mark box says a new run arrives with nowhere to go.
24. Every code deploy kills agents that have run for hours, and the team wants those agents to finish rather than be restarted. Which change fits?Analyze
A card for agent run 118 that has been working for six hours and twenty minutes, next to a card for a deploy event that restarts all workers now, both pointing down to a box marked run 118 killed.
25. Users pay per agent run, and the charge API accepts an idempotency key. Which design gives exactly-once effective charging?Design
A requirement card stating exactly one charge per agent run and that the charge API accepts an idempotency key parameter, with a question mark over the service design that satisfies it.
26. You must route messages for thousands of agents through queues without flooding any one queue. Which layout follows MegaAgent's two layers?Design
Many small agent dots around a circle, all with a line into a single shared queue box, next to a question mark box labelled overflow.

Build It

main.py implements:

  • CheckpointStore: a SQLite-backed checkpoint log keyed by thread_id. Each super-step writes a row.
  • run_agent_with_checkpoint(store, thread_id, ...): runs a tiny agent that counts up to a goal, can simulate a crash at a chosen super-step, and on the next call resumes from the last checkpoint.
  • AgentQueue: a per-agent Idle / Processing / Response state machine with an in-queue and an out-queue. It lives in memory and advances one transition per step() call.
  • demo_async_vs_threads(): runs 500 simulated "LLM calls" (a 50 ms sleep each) with asyncio and with threads, and prints the wall-clock time for each plus the ratio.

Run:

python3 main.py

What you should see: the first worker crashes at super_step 3, the last checkpoint holds counter 3 at super_step 2, and the second worker resumes and finishes at 5. The queue demo walks one agent through two messages. The benchmark prints async finishing close to the 50 ms sleep, with threads slower by a small multiple on my run. It does not measure memory. The ~1MB per thread stack in the printed ratio line is a stated figure, not a measurement.

Check: Build It0/7
1. What does the toy async-versus-threads benchmark actually measure?Explain
Code for the benchmark's two simulated calls, sim_llm_call_async using asyncio dot sleep and sim_llm_call_sync using time dot sleep, each a 0.05 second delay, with a note that 500 of them are started together.
2. In main.py, worker 1 runs thread t-1 with crash_at=3 and goal=5. What does store.latest("t-1") return afterwards?Apply
A terminal capture reading worker 1 starts thread t-1 targeting counter 5, then worker crashes at super_step 3, with a question mark over what store dot latest of t-1 returns afterward.
3. After worker 1 crashes, worker 2 calls run_agent_with_checkpoint(store, "t-1", goal=5). How many new checkpoint rows does it write?Apply
A terminal capture reading worker 2 resumes thread t-1, then worker 2 completed with final counter 5, with a question mark over how many new checkpoint rows were written in between.
4. A fresh AgentQueue gets two enqueued messages, then step() is called twice. What state is it in?Apply
The same unlabelled three-state cycle, with an in-queue starting at two messages and a note that step has been called twice so far, next to a question mark over the current state and both queue sizes.
5. Continuing the same two-message demo, what are the state and queue sizes after seven step() calls in total?Apply
The same unlabelled three-state cycle and two starting messages, now with a note that step has been called seven times in total, next to a question mark over the current state and both queue sizes.
6. The benchmark starts 500 concurrent 50 ms sleeps with asyncio.gather. About how long does the async run take?Apply
A box for asyncio dot gather feeding many thin overlapping lines, each about a 50 millisecond wait, with a question mark over the total wall-clock time.
7. A teammate says the main.py checkpoint demo proves the design is ready for production failover. What does the demo leave untested?Analyze
A card listing facts about the checkpoint demo: it writes JSON state to a SQLite file keyed by thread_id and super_step, worker 2 reads the last row and continues, and the only state change is a counter increment. A question mark asks what this leaves untested.

Use It

outputs/skill-scaling-advisor.md helps you choose a durable-execution approach: FastAPI + Postgres, LangGraph runtime, Temporal, or a custom build. It weighs load, how long state has to be retained, and how often you deploy.

Ship It

Production hardening:

  • Start simple (Bedi's rule). FastAPI + Postgres until you measure it failing.
  • Instrument before you optimize. Per-run latency histogram, per-step time, retry count, failure categories.
  • Use an outbox for side effects, especially payments and external API calls.
  • Deploy rainbow-style so in-flight agent runs survive a deploy.
  • Adopt a durable-execution engine (Temporal / LangGraph / Restate) when you hit a specific problem: hour-long human-in-the-loop waits, cross-region coordination, or complex retry and compensation policies.
  • Keep the I/O layer async. Reserve threads for CPU-bound post-processing.
Check: Ship It0/2
1. A team expects about 40 concurrent agent runs, each a few minutes long with no human waits, and nobody has measured a failure yet. What should they build?Design
A stat card for a team's current load: about forty concurrent runs, each a few minutes long, no human-in-the-loop waits, and no measured failures yet, with a question mark over what they should build.
2. A product has agents that wait hours for humans, needs cross-region coordination, and requires complex retry and compensation policies. A FastAPI + Postgres version is already failing under it. What fits?Design
A stat card for a different team: human-in-the-loop waits that last hours, coordination across regions, complex retry and compensation policies, and their current FastAPI and Postgres design already failing, with a question mark over what fits.
Author's Note

I'd run main.py and read the checkpoint demo first. The crash is raised before the write for super_step 3, so the store holds steps 0 through 2 with counter at 3. Worker 2 starts at step 3 and finishes at 5. That is the durable-execution idea in under fifty lines of SQLite.

The toy also skips the hard part. Nothing in it decides that worker 1 is dead, so there is no lease. The only state change is a counter increment, which is safe to repeat, so idempotency never gets exercised. The outbox exercise is where it starts to matter.

I'd read the async-versus-threads numbers as a direction, not a benchmark. Each "call" is a 50 ms sleep, so you are timing 500 waiters being scheduled, not real token streaming. AgentQueue is the same kind of sketch. It shows the Idle / Processing / Response shape from MegaAgent, but it is a pair of Python lists in one process and nothing survives a restart.

For a real system I'd start where Bedi does and wait for a measured failure before reaching for an engine.

Exercises

  1. Run main.py. Confirm checkpoint resume works; measure async vs thread concurrency difference.
  2. Implement an outbox table: every tool call writes to outbox first, then a separate async task executes. Verify idempotency by running the tool call twice.
  3. Simulate a rainbow deploy: two concurrent runtime versions; route half of new thread_ids to each; confirm that in-flight threads on the old version are not interrupted.
  4. Read LangGraph's runtime doc (linked below). Identify which features of the runtime would take the longest to replicate in a hand-rolled FastAPI + Postgres version. Is that a reason to adopt, or can you defer?
  5. Read MegaAgent (arXiv:2408.09955) Section 3. The two-layer coordination (intra-group + inter-group admin chat) is explicit. Sketch how you would map this to a message queue with two queue families.

Key Terms

TermWhat people sayWhat it actually means
Durable execution"Persist the program state"Engine writes state after each super-step; crash recovery is deterministic.
Super-step"Transactional boundary"Unit of work between checkpoints. LangGraph term.
thread_id"Agent run identifier"Key that binds checkpoints and resume logic.
Idempotency"Safe to retry"Repeating a side effect produces the same result as one attempt.
Outbox pattern"Decouple side effects"Write intent to a table; a separate executor performs and marks done.
At-least-once delivery"Possible duplicates"Message queue semantics; dedup key makes consumer effective-once.
Rainbow deploy"Overlapping versions"Multiple runtime versions concurrent during long-running workloads.
Async fiber"Cooperative yielding"User-mode concurrency; cheap compared to threads for I/O-bound loads.
Checkpoint"State snapshot"Serialized state at a super-step boundary; key for resume.

Further Reading

Code

Ships With