Queueing Agent Jobs with Celery or a Message Broker
Agent runs are multi-second to multi-minute workflows with variable turn counts.
Putting them on a job queue decouples HTTP intake from model and tool work so you can retry, scale workers, and survive process restarts.
This recipe uses Celery + Redis as a concrete broker pattern. The same design applies to RQ, Dramatiq, ARQ, Sidekiq, SQS + workers, or cloud task queues.
Summary
Accept agent work as durable jobs, process them in workers with bounded concurrency, persist status and results, and let clients poll or subscribe instead of holding open request connections through the full loop.
Recipe
- Define a job payload:
job_id,goal,tenant_id,priority,trace_id, optionalmodel_hint. - Run a message broker (Redis, RabbitMQ, SQS, etc.) as the source of truth for pending work.
- Enqueue from the API after auth, validation, and admission checks.
- Implement a worker task that runs the agent loop and writes status transitions.
- Cap worker prefetch/concurrency so one process does not open unbounded model streams.
- Configure retries only for infrastructure failures, not for "model gave a bad answer."
- Store results outside the broker (DB/object store); treat messages as commands, not archives.
- Emit metrics: enqueue rate, depth, time-in-queue, run duration, failure reason, cost.
Working Example
Minimal Celery-shaped sketch. Install versions at build (celery, Redis broker). Adjust imports if your stack differs.
# tasks.py - teaching sketch; verify Celery API at build
from celery import Celery
from dataclasses import asdict, dataclass
import time
import uuid
app = Celery("agents", broker="redis://localhost:6379/0", backend="redis://localhost:6379/1")
app.conf.worker_prefetch_multiplier = 1
app.conf.task_acks_late = True
@dataclass
class AgentJob:
job_id: str
goal: str
tenant_id: str = "default"
def run_agent_loop(goal: str) -> str:
time.sleep(0.05) # stand-in for model + tools
return f"done:{goal[:40]}"
@app.task(bind=True, max_retries=3, name="agents.run")
def run_agent_job(self, payload: dict) -> dict:
job = AgentJob(**payload)
try:
result = run_agent_loop(job.goal)
return {"job_id": job.job_id, "status": "done", "result": result}
except ConnectionError as exc:
# infrastructure only - not quality failures
raise self.retry(exc=exc, countdown=2 ** self.request.retries)
def enqueue_agent(goal: str, tenant_id: str = "default") -> str:
job = AgentJob(job_id=str(uuid.uuid4()), goal=goal, tenant_id=tenant_id)
run_agent_job.delay(asdict(job))
return job.job_id
if __name__ == "__main__":
print("enqueued", enqueue_agent("triage support ticket"))Run a worker in another process (verify flags at build):
celery -A tasks worker --loglevel=INFO --concurrency=2Deep Dive
Why queues fit agents
| Concern | Sync HTTP handler | Queued job |
|---|---|---|
| Duration | Hits gateway timeouts | Worker owns long loops |
| Retries | Awkward mid-response | Broker redelivery + policy |
| Scale | Scale web tier with work | Scale workers independently |
| Spikes | Drop or melt | Buffer with depth limits |
| Observability | One request span | Job lifecycle + run traces |
Agents are closer to workflows than to CRUD handlers.
Broker choices
| Broker | Strength | Watch-outs |
|---|---|---|
| Redis + Celery/RQ | Simple ops for many teams | Persistence and multi-queue design need care |
| RabbitMQ | Routing, ack semantics | More moving parts |
| SQS / cloud tasks | Managed durability | Visibility timeouts vs long agent runs |
| NATS / Kafka | High throughput streams | Different ack/consumer models; often overkill for job RPCs |
Pick durability and ops familiarity first. Agent payloads are usually small; throughput is limited by model RPM before broker limits.
Job payload design
Include:
- Stable
job_id(also use as idempotency key). tenant_idfor quotas and noisy-neighbor isolation.trace_id/run_idfor observability.- Priority only if you will honor it end-to-end.
- Version of prompt/tool schema if workers can be mixed during deploys.
Exclude:
- Huge context blobs (store in object storage; pass a pointer).
- Secrets (workers load from a secret manager).
- Mutable "live conversation" state without a single writer.
Ack, visibility, and long runs
Long agent jobs interact badly with default timeouts:
- Late ack (ack after success) avoids losing work on crash, but can redeliver.
- Visibility timeout on SQS-style systems must exceed worst-case run or you need heartbeat extension.
- Idempotent tools matter because at-least-once delivery will double-run side effects.
Design tools as if every job may run twice.
Retries: split the classes
| Failure class | Retry from queue? |
|---|---|
| Broker blip, worker OOM mid-run | Yes, with backoff and cap |
| Provider 429 with Retry-After | Often yes, delayed |
| Invalid user input | No - fail terminal |
| Model quality / wrong answer | No - that is eval/product, not infra |
| Tool business error (not found) | Usually no |
Stacking job retries on top of tool retries and model re-turns creates storms. Share budgets with Concurrency Limits and Backpressure for Agent Workers and reliability breakers.
Status API shape
Typical states: queued → running → done | failed | cancelled.
Expose:
job_id,status, timestampsstop_reasonwhen failed/cancelled- Result pointer or inline small result
- Optional progress events (turn count) via pub/sub or SSE on a side channel
Multi-queue patterns
- Fast lane: short FAQs, cached answers, cheap models
- Default lane: normal agents
- Heavy lane: browser, code sandboxes, multi-agent graphs
Separate queues prevent one heavy class from starving interactive work. Workers can subscribe with different concurrency.
Gotchas
- Prefetch > 1 with long jobs. Workers reserve many jobs then sit busy; set prefetch low for agents.
- Using the broker as the result archive. Messages expire; persist results properly.
- Retrying non-idempotent side effects. Duplicate emails and double charges.
- Visibility timeout shorter than the agent. Another worker steals an in-flight job.
- No admission control. Infinite queue depth becomes infinite user wait.
- Passing full chat history in every message. Payload bloat and PII sprawl; store and reference.
- Same concurrency for all job types. Browser jobs and cheap classifiers need different caps.
Alternatives
| Approach | Strength | Weakness |
|---|---|---|
| Celery/RQ + Redis (this recipe) | Familiar Python path | You own broker ops |
| Temporal / durable workflows | Strong history and timers | Heavier platform commitment |
| Cloud Tasks / SQS workers | Managed scale | Timeout and local dev friction |
| Always-on websocket agent process | Low latency chat UX | Harder horizontal scale and recovery |
In-process asyncio.Queue only | Great for demos | Dies with the process; no multi-host |
Many products combine a durable job system for work with websockets only for streaming tokens of an already-accepted job.
FAQs
Do I need Celery specifically?
No. You need a durable queue, workers, and a result store. Celery is a common Python default, not a requirement.
Should the API wait for the agent to finish?
Prefer 202 + job_id for anything that can exceed a few seconds. Stream tokens from the worker after accept if UX needs it.
How many retries is safe?
Start with 0-2 for infra failures with exponential backoff. Never unlimited. Quality issues need product policy, not redelivery.
Where does LangGraph or CrewAI sit?
Inside the worker task. The queue owns scheduling; the framework owns the agent graph (verify current deploy patterns at build).
How do I cancel a running job?
Persist a cancel flag workers check between turns, and revoke the broker task when supported. Pair with kill-switch design in human-in-the-loop content.
What about priority customers?
Use separate queues or weighted fair queuing. Priority fields without worker policy are decoration.
Can I run workers on serverless?
Only if max duration and cold starts fit your agent. Long loops often prefer always-on workers or durable workflow platforms.
How do I test queue code?
Unit-test the pure agent function; integration-test enqueue → worker → result with a test broker or eager mode where available.
What metrics matter first?
Queue depth, time-in-queue p95, success rate, cost per job, and provider 429 rate.
How does this relate to horizontal scale?
Queues are the front door of a worker pool. See Architecture Guide: A Horizontally Scalable Agent Worker Pool.
Related
- Scaling Agent Systems Basics - in-process queue intro
- What Actually Bottlenecks an Agent System at Scale - why decoupling matters
- Concurrency Limits and Backpressure for Agent Workers - caps after enqueue
- Architecture Guide: A Horizontally Scalable Agent Worker Pool - full pool design
- Scaling Agent Systems Best Practices - checklist
- Timeout Strategies for Long-Running Agent Steps - time bounds inside jobs
- Long-Running Agents on a VPS: Process Management and Restarts - host process care
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.