Agents spend about 95% of their life waiting. For an API. For a schedule. For a human who will get to it tomorrow.

A waiting agent is the most expensive way ever invented to run agents.

What we tried first, and why it was wrong

I didn't arrive at this cleanly. Like everyone else, we spent a long time trying to keep agents alive. Reconnect logic. Retry loops. Process supervision. Babysitting.

Then it clicked that we were solving the wrong problem.

Every one of those techniques is an investment in keeping something running that has nothing to do. You are paying engineering effort to preserve an expense.

Agents shouldn't stay alive. They should act when triggered.

And the architecture that does it is not new. It's 1980s queue infrastructure applied to agents. I didn't invent any of it. I recognised that a forty-year-old pattern already solved the problems everyone is currently reinventing badly.

Following along visually. I drew this as one architecture diagram and walked through it in AI Agents Don't Need New Infrastructure. Every section below maps to a box on it.

Open it in another tab if you want the picture. If you'd rather just read, nothing here depends on seeing it.

Everything becomes an event

Four producers, all doing the same thing: dropping a message in a queue.

Schedulers. A job needs to run at 9am. The scheduler doesn't call the agent. It drops a message at 9am. A timer is just an event source.

External systems. This is the one most people get wrong. The agent asks an API for something and waits. Don't wait. Put it to sleep. When the webhook lands it arrives with a correlation ID, enters the queue, and wakes the agent. A reply is just another event.

Peer agents. Agent A finishes a step, that result is an event, Agent B consumes it. That is your multi-agent coordination. Not the elaborate orchestration diagrams you see online. Actors communicating through a queue.

Users and apps. Button clicks, uploads, approvals. Commands become events. And when someone asks where the human fits: the human is just another actor.

"What about synchronous calls?" Once a queue is in the middle, nothing is synchronous. You've forced it async. Request and reply still work, nothing blocks, and the correlation ID reattaches the response to the conversation that asked.

One genuine exception: live chat. That is actually synchronous, so call the agent directly. Everything else goes through the queue.

Here's the thing about that queue, though. The clever part is not the queue at all. It's what you put inside each message.

The real engineering is in the envelope

Not in the agent. Every event carries five fields.

  • type what just happened

  • session key which conversation. Drives routing and ordering

  • correlation ID which request this answers

  • idempotency key have I processed this exact event before

  • payload the only part of this entire architecture that is AI

Read that last line again. Everything else is messaging discipline that has worked in distributed systems for forty years. We don't need to recreate it. We need to reapply it.

Those fields answer three classic distributed failure modes: ordering, reply matching, and duplicates. Get the envelope right and most of the hard problems are solved before the model is involved.

Most, not all. Ordering only holds if you understand how the queue splits the work underneath, and that is the one place I see people get burned.

Partitions, and the one warning

One topic, keyed by session key, split across partitions. Think parallel conveyor belts. Same key always lands on the same belt.

Ordering is only guaranteed within a partition. Across belts, no promises. Within a belt, strict order.

That's the trade: you give up global ordering to get throughput, and you keep per-key ordering. Which is why the key is per conversation. Choosing the key is how you choose what stays ordered.

So that's the infrastructure. Now the part nobody draws on the diagram, which is what the agent is allowed to hold and what it must never touch.

What the agent holds, and what it never holds

Now the part that isn't on the diagram, and it took me a while to say it out loud properly.

Ask what tools an SDR needs. It has to write an email. It has to send an email. It has to post a notification in Slack. Fine. Now ask which of those things touches your customer data.

The agent has access to memory.

The tools never do.

The agent holds the state. It reads the store, it knows the history, it decides. Then it calls a tool and hands over exactly the arguments that tool needs. The tool that writes the email never sees your database. It gets a brief and returns text.

So tools are bonded to intent, not to the agent. "Write an email" is a capability anything can call. It carries no privilege because it holds no data.

That does two jobs at once. It keeps the blast radius of a compromised tool near zero, and it means a stateless worker can rehydrate and call the same tools without any of them caring which conversation this is.

The runtime is deliberately boring

Stateless workers. Scale by adding consumers. Every invocation is the same three steps.

Rehydrate. The worker wakes up empty and remembers nothing. The queue delivers the trigger, the state store delivers the context, loaded by session key.

People object here: agents need context, statelessness breaks that. No. The agent remembers nothing. The system remembers everything. That's good design, not a limitation.

The agent loop. Plan, tool, observe. This is where the LLM call lives, and it is the only genuinely agentic box in the whole architecture.

The outbox. Before any side effect, an email, a payment, an expensive API call, check the idempotency key. Seen before, skip it.

Because the model is probabilistic, you cannot prevent duplicate actions at the model layer. Instruction doesn't guarantee behaviour, and injection can override it. So you gate the tool instead.

That covers the machines. The other actor in this system is a person, and the design gets a lot more demanding once you look at where they sit.

Where the human actually sits

"The human is just another actor" sounds like a throwaway line. It has a real shape, and the number behind it is why.

When someone replies to an outbound email, you have minutes. Response rates drop sharply after the first 15 to 30 minutes, and they keep dropping. So a human approval step that takes four hours has silently destroyed the thing it was protecting.

What that means in practice: the AI drafts, an event fires, Slack gets pinged, and anyone in the channel can click a deep link straight into a priority queue. Approve, or edit and approve. Nobody sits watching a dashboard.

Two details make it work. The queue is ranked by lead score, so the limited human attention goes to the highest-value item instead of whatever arrived first. And you store both the AI draft and the human edit. That diff is latent training data you are otherwise throwing away every single time somebody fixes a sentence.

Three guarantees you get for free

These fall out of the design. You do not write them.

Concurrency. Same partition, same worker, so no two instances race on one conversation. You wrote zero locks.

Reliability. Crash mid-task and the event redelivers; the worker replays from the last checkpoint. You wrote zero recovery code.

Back pressure. Events arriving faster than agents can process doesn't crash anything and doesn't drop anything. The queue gets deeper, and consumer lag tells you exactly how far behind you are.

Normally each of those is a project. Here they're consequences.

What this costs you

Three guarantees for free, and then the invoice. I have paid all of these.

  • No stack trace crosses a queue. One request becomes six events across four workers. Debugging means distributed tracing, and if you don't have it, your first week here is spent building it instead of shipping agents.

  • Idempotency is a discipline, not a feature. Every consumer, every new tool, forever. One engineer skips the outbox check on a Friday and you have double-charged somebody.

  • Cold start plus rehydrate lands on every wake. Zero cost while idle is real, right? So is the latency you traded for it.

  • The session key ceilings throughput per conversation. Same key, same belt, strict order. That is the guarantee you wanted, and it means one very busy conversation cannot be parallelized no matter how many consumers you add.

  • Splitting memory from tools costs you a design conversation per tool. Somebody has to decide what arguments each tool receives. Do it lazily and you will hand a tool the whole record because it was easier, which puts the data back where you just removed it from.

  • Local development gets worse. You cannot just run the agent. You run a queue, a store, and a worker, or you run against shared infrastructure and step on a teammate.

  • When to skip it. Live chat is genuinely synchronous. And below a certain scale, a cron job calling a script is the correct architecture. Do not build a queue for four events a day.

The distinction most people collapse

Stateless workers don't mean no state. State lives in the store, not the process. That's what makes workers cattle instead of pets: kill one and it restarts, rehydrates, and idempotency stops it redoing completed work.

The queue answers when the agent acts. The store answers what it knows.

Do not confuse them and do not couple them.

From what I've seen, confusing those two jobs is the single most common design mistake in agent infrastructure.

When an agent finishes, its results don't return anywhere. They publish back as new events, which makes the agent just another producer. Chains, fan-outs, and multi-agent pipelines are all the same thing once the substrate is right.

The full walkthrough. AI Agents Don't Need New Infrastructure, with the architecture diagram on screen and the envelope fields walked one at a time.

Chris

Keep reading