Agent Backend Architecture at a Glance: How APIs, Queues, Workers, State, and SSE Fit Together

Execution architecture boundaries — mental model, failure modes, optional deep dives

How API, queue, worker, PostgreSQL, Redis, and SSE divide responsibility in an agent backend. Implementation detail lives in follow-up articles.

The previous article, Agent Pattern Design, covered control-flow patterns inside a run: tool use, parallelism, planning, HITL, retry, and reflection.

This article has a different job. It is not how to implement a production agent backend, but how to split responsibilities and boundaries so later implementation articles have a shared map.

1. When agent runs get long, the problem changes

At first, running an agent inside a FastAPI handler is fine.

When a run takes three minutes instead of thirty seconds, the story changes. The user may close the browser, a Pod may restart, or the same request may arrive twice. The backend stops being a simple LLM app and becomes a small distributed system.

Production questions follow quickly:

  • Should an HTTP handler hold the run until it finishes?
  • When the user closes a tab, should execution stop?
  • Where do progress and checkpoints live, and what do we show in the browser?
  • If an API Pod restarts, how do we rediscover in-flight runs?

The mental model below is the starting point.

API        = accept work
Queue      = preserve execution intent
Worker     = execute the run
PostgreSQL = durable state / source of truth
Redis      = runtime state / coordination / short-lived events
SSE        = deliver progress to the user

The main questions this article answers:

Why do we need a queue?
Why does duplicate execution happen?
Why is the gap between DB and queue dangerous?
Why must SSE reconnect be designed for?
LayerThis articleFollow-ups
Concurrencyasync/thread/process vs worker roleWhy Agent Systems Need Async, Async, Threads, and Processes
Accept & execute202, queue, worker ownershipWorker Is Not a Thread
StatePostgreSQL vs RedisWhere Agent State Lives, State Is Not an Event
DeliverySSE and observationAgent SSE Streaming
Side effectsSurviving duplicatesCan an Agent Tool Execute Exactly Once?

2. The architecture map

Solid arrows are how the user commands execution. Dashed arrows are how the browser observes progress.

Browser → API → Queue → Worker
              ↓
         PostgreSQL · Redis → SSE → Browser
Figure 1. Agent backend — command flow (solid) and observation flow (dashed)
  • Solid arrow — Command: user → API → Queue → Worker
  • Dashed arrow — Observation: Worker → stores → SSE → user
BrowserCommand with POST · observe with GET/SSE
API DeploymentShort request lifetime
  • POST auth, validation, create run
  • GET read PostgreSQL snapshot
  • SSE relay events after cursor
Queue · execution requestclaim · lease · retry · redelivery
Worker DeploymentRun-attempt lifetime
  • Own this attempt with attempt_id and lease
  • Run agent graph, LLM calls, and tools
  • Persist checkpoint/result before ack
PostgreSQLrun · JSONB snapshot · checkpoint · result · auditSystem of record across restarts
Redisprogress · cancel · lock · cursor · bounded StreamSmall, expiring coordination data
LLM · tools · business DBsNetwork I/O · side effectsTimeout, retry, idempotency required
Observation path · does not own execution
Workersnapshot · eventsGET · SSE APIBrowser

A disconnect does not stop the run; reconnect recovers from snapshot + last event cursor.

Command:     Browser ──POST──> API ──publish──> Queue ──claim──> Worker
Observation: Browser <──SSE── API <──read──── Redis / PostgreSQL <──write── Worker

The worker runs LLM calls, tools, and checkpoints. The API mostly accepts runs, publishes to the queue, serves reads, and relays SSE. That split lets API and worker replica counts track different resources.

Command path vs observation path

PathFlowRemember
CommandBrowser → API → Queue → WorkerThe HTTP handler does not own run lifetime
ObservationWorker → state/event store → SSE → BrowserA disconnect does not have to stop the run

3. The API accepts; it does not execute

Demos often start with:

asyncio.create_task(run_agent(request))
return {"status": "started"}

From a pattern view, a loop is running. From a backend view, run_id, failure lookup, and survival across deploy are still undefined.

Starting a long run usually follows:

  1. Validate and authorize
  2. Insert a run row in PostgreSQL
  3. Publish an execution message to the queue
  4. Return 202 Accepted and run_id
@app.post("/runs", status_code=202)
async def create_run(request: RunRequest, user: User = Depends(current_user)):
    authorize(user, request)
    run_id = new_run_id()
    await run_store.create(run_id=run_id, user_id=user.id, status="submitting", input=request)
    await job_queue.publish({"run_id": run_id, "input": request.model_dump()})
    await run_store.mark_queued(run_id)
    return {"run_id": run_id, "status": "queued"}

This snippet is a minimal illustration of responsibility split. In production you must handle the dual-write gap between DB writes and queue publish.

202 means the request was accepted and processing continues asynchronously—not that the browser connection guarantees survival. Queues, workers, and durable state provide that.

FastAPI Background Tasks suit short post-response work; agent runs usually belong in a queue.

4. The queue separates execution lifecycle

The queue’s job is not to run work faster, but to separate the API request lifecycle from the agent execution lifecycle.

  • The API submits a job.
  • The worker consumes it.
  • If the worker dies, the job may be redelivered.
  • Duplicate execution is therefore a normal failure mode.

Steps with side effects must ask “is this the first processing of this message?” via run rows, attempts, or idempotency keys. Can an Agent Tool Execute Exactly Once? continues that thread.

Deep dive — Why can a queue lead to duplicate execution?

Brokers usually offer at-least-once delivery:

  • If a consumer dies before ack, the message reappears
  • After lease or visibility timeout, another worker may claim the same job
  • Retries may publish twice

Do not expect the queue to dedupe; design idempotent steps and explicit duplicate detection. Ack timing, idempotency keys, and lease expiration are wired in implementation articles.

5. The worker owns the agent run

The worker role claims queue jobs and owns the current attempt lifecycle: LLM calls, tool loop, checkpoints, and terminal status commits.

Calls to external LLMs or business databases are not the same as internal state transitions—they need timeouts, retries, and idempotent side effects.

Closing a tab or dropping SSE removes the observation path only. An accepted run typically keeps going. Cancellation is product policy; the default is run lifetime ≠ HTTP connection lifetime.

6. Where does state live?

Do not put every kind of state in one store.

ComponentMain responsibility
PostgreSQLDurable run state, checkpoints, results, audit
RedisRuntime state, coordination, short-lived event history
QueueExecution intent / delivery
Object storageLarge outputs and artifacts (when needed)

PostgreSQL holds recoverable truth. Redis holds fast-changing progress and coordination.

Retrieval storage (embeddings, RAG indexes) is a separate concern; this article covers execution architecture only.

Deep dive — What if the DB commit succeeds but queue publish fails?

Separate run_store.create and job_queue.publish calls leave gaps:

  • A run row without a queue message if the process dies after commit
  • A message without a row if the transaction fails after publish

Using DB and queue separately introduces a consistency problem. Production systems often use a transactional outbox:

BEGIN;
  INSERT INTO agent_runs (...);
  INSERT INTO outbox (topic, payload) VALUES ('run.execute', ...);
COMMIT;
──> publisher worker reads outbox, publishes to queue, sets processed_at

Before returning 202, the system must leave execution intent you can rediscover later.

Deep dive — How do you update Redis progress and event streams atomically?

At map level, Redis is for runtime coordination. In implementation, teams often combine a Hash (current snapshot), a bounded Stream (recent events), and Lua scripts for atomic updates. TTL, stream limits, and consumer offsets connect in Agent SSE Streaming and State Is Not an Event.

Mid-article recap: The API accepts work, the queue delivers it to a worker, durable state records outcomes, and SSE shows that state to the user.

7. Separate execution from observation — SSE

SSE is independent of agent execution.

  • If the browser disconnects, the worker should keep running.
  • On return, the user needs the current snapshot or recent events again.
Worker → State / Event Store → SSE endpoint → Browser

The observation path sends id, event, and data per the WHATWG SSE format. When a generator sees disconnect, it stops HTTP chunks only. A broken stream is not a run rollback.

Deep dive — After SSE reconnects, how are events recovered?

Clients reconnect with Last-Event-ID; servers resume from an event-store cursor. Redis Streams, sequence numbers, replay windows, and transport vs domain idempotency are developed in Agent SSE Streaming.

8. What if two workers run the same run?

Queues and leases alone do not prove an old worker is gone. Design for multiple workers touching the same run.

Deep dive — Worker A was presumed dead; B took over—then A wakes up late?
Worker A → fencing token 41
Worker B → fencing token 42

Storage accepts only token >= current token

A write rejected · B write accepted

Fencing tokens, monotonic versions, and queue leases reduce stale writers overwriting fresh progress. Comparing only updated_at does not prove a write belongs to the current attempt. See Where Agent State Lives.

9. Pod, process, worker, thread

When Kubernetes, Python, and agent vocabulary land in one sentence, designs wobble. A worker is not a Pod.

TermMeaning
PodKubernetes scheduling / deployment unit
ProcessOS execution unit
WorkerJob execution unit (queue consumer role)
Thread / async taskConcurrency primitive inside a process

When a Pod restarts, its processes and asyncio tasks go with it. The worker role is designed so queue messages and PostgreSQL runs let another Pod resume. A “thread pool worker” is not the same as a queue consumer.

10. Async, thread, process — inside the worker

ToolWhen
asyncOverlap many I/O waits in one process
threadIsolate blocking SDK calls
process / separate workerCPU-heavy or isolated execution

Python GIL and executor details continue in Why Agent Systems Need Async and Async, Threads, and Processes.

11. Why split API and worker deployments

Locally, API and worker in one process or Pod is enough to learn. In production, split Deployments.

Figure 2. Dev (single process/Pod) vs prod (separate API and Worker Deployments)

Development

API and worker may share one process or one Pod

Process / PodHTTP handler + queue consumer + agent runs
  • Responsibilities stay separate in code
  • Deployment and failure lifetime are shared

Production

Separate Deployments and Pod sets — scale, deploy, and fail independently

API Deployment
API Pod 1
API Pod 2
API Pod 3
Scale on HTTP load
Worker Deployment
Worker Pod 1
Worker Pod 2
Queue depth, CPU/GPU, external API load
Shared execution planeQueue · PostgreSQL · RedisKeeps runs discoverable after API Pod replacement
APIWorker
Scale onHTTP request volumeExecution workload, queue depth
Podsagent-apiagent-worker
SharedQueue · PostgreSQL · Redis

Putting everything in one Pod makes independent scaling hard.

12. Graceful shutdown

Pod termination is not just “kill the process”—it is an in-flight run ownership problem.

  1. Stop accepting new jobs
  2. Finish or hand off running jobs
  3. Save checkpoints
  4. Decide ack vs requeue
  5. Exit

After SIGTERM, align lease release and ack order with Pod Lifecycle behavior. Do not ack a queue message before committing an important result to PostgreSQL.

13. Architecture recap

API        → accept (202 + run_id)
Queue      → separate lifecycle, redelivery possible
Worker     → own attempt, run tool loop
PostgreSQL → durable truth
Redis      → runtime / coordination / short observation
SSE        → observation only, not execution owner

Failures on the same axes:

Failure modeRemember
Duplicate executionRedelivery is normal; design for idempotency
Dual write (DB ↔ queue)Tie acceptance to publish, e.g. outbox
Stale progressReject late writes with attempt/version/fencing
SSE disconnectRun continues; reconnect restores snapshot/events

Tool loops, HITL, and retry from pattern design run inside the worker. This map fixes which infrastructure owns lifecycle around that loop.

Figure 3. One run through the system
  1. 1Browser → APIPOST /runsAuthenticate, authorize, and validate input
  2. 2API → PostgreSQLsubmittingPersist run_id and recoverable execution intent
  3. 3API → QueuequeuedExecution intent lives outside the API process
  4. 4API → Browser202 + run_idSafe acceptance, not completion
  5. 5Queue → Workerclaim → runningStart this attempt with attempt_id and lease
  6. 6Worker → Storescheckpoint · progress · eventPostgreSQL is durable; Redis is short-lived observation data
  7. 7Worker → PostgreSQLcompleted / failedCommit terminal result before queue ack
  8. 8API · SSE → Browsersnapshot + cursorRead, reconnect, and ignore duplicate events

14. Next: implementation

This article defines architecture boundaries only. Follow-ups implement, in code:

  • FastAPI submit endpoint
  • Queue publisher (or outbox relay)
  • Worker process
  • PostgreSQL run state
  • Redis progress state
  • SSE streaming endpoint
  • Retry and idempotency

Next, Why Agent Systems Need Async covers overlapping I/O waits inside the worker. The full path is on the Agent Engineering series page.

Sources / Data / Code

References

  1. Deployments — Kubernetes Documentation
  2. Pod Lifecycle — Kubernetes Documentation
  3. Background Tasks — FastAPI Documentation
  4. PostgreSQL JSON Types — PostgreSQL Documentation
  5. Redis Streams — Redis Documentation
  6. Server-Sent Events — WHATWG HTML Standard

Discussion

Discussion

Have a question, correction, counterexample, or another interpretation?