Background Execution

Accept runs with background=true, poll or stream them, and make acceptance durable with a database-backed job queue.

Submit a run with background=true, stream=false and AgentOS returns acceptance with a run_id. The run executes on the server while your client polls, streams, or disconnects. Add QueueConfig(durable=True) and that acceptance becomes a committed database row that survives crashes and deploys.

db stores runtime state, queue.redis coordinates replicas, and durable=True persists accepted queueable jobs.

PartSettingWhat it holds
TruthAgentOS(db=...)Sessions, run rows, and the agno_jobs queue table. Recovery requires healthy storage and an available matching worker.
CoordinationQueueConfig(redis=...)Sets both the event stream (RedisEventStream) and the cancellation manager (RedisRunCancellationManager) on every replica. Ephemeral.
Durable acceptanceQueueConfig(durable=True)A committed job row per accepted run. Whichever replica claims it executes it.

Quickstart

uv pip install "agno[os]" openai psycopg

Run Postgres:

docker run -d \
  -e POSTGRES_DB=ai \
  -e POSTGRES_USER=ai \
  -e POSTGRES_PASSWORD=ai \
  -p 5532:5432 \
  --name pgvector \
  agnohq/pgvector:18
durable_queue.py
from agno.agent import Agent
from agno.db.postgres import PostgresDb
from agno.models.openai import OpenAIResponses
from agno.os import AgentOS, QueueConfig

db = PostgresDb(db_url="postgresql+psycopg://ai:ai@localhost:5532/ai")

agent = Agent(
    name="Durable Agent",
    id="durable-agent",
    model=OpenAIResponses(id="gpt-5.5"),
    db=db,
)

agent_os = AgentOS(
    agents=[agent],
    db=db,
    queue=QueueConfig(durable=True),
)
app = agent_os.get_app()

if __name__ == "__main__":
    agent_os.serve(app="durable_queue:app", reload=True)

Submit a run:

curl -X POST localhost:7777/agents/durable-agent/runs \
  -F "message=Write a haiku about queues" \
  -F "background=true" \
  -F "stream=false"
{"run_id": "20a47fb2-...", "session_id": "72b8bc0c-...", "status": "PENDING"}

The 202 is returned after the queue row commits. Poll for the result:

curl "localhost:7777/agents/durable-agent/runs/{run_id}?session_id={session_id}"

The response carries status (PENDING, RUNNING, PAUSED, COMPLETED, CANCELLED, or ERROR) and, once finished, content.

The 202 contract

Non-streaming background submissions, durable or not, return this acceptance shape. Streaming submissions return SSE:

FieldMeaning
run_idIdentifier for polling, resuming, cancelling, and continuing. Never changes for the life of the run.
session_idSession the run belongs to. Required on poll.
statusPENDING on a fresh acceptance.

The background and stream form fields select the execution mode:

backgroundstreamResponse
truefalse202 with run_id. Poll GET /agents/{agent_id}/runs/{run_id}.
truetrueSSE stream of the run's events. Disconnect and reconnect through POST /agents/{agent_id}/runs/{run_id}/resume with last_event_index.
falsetrueInline SSE. A client disconnect requests cancellation.
falsefalseInline execution with one JSON response; client disconnection does not guarantee cancellation.

Teams and workflows use the same fields under /teams/{team_id}/runs and /workflows/{workflow_id}/runs. See Background Execution for the SDK-level arun(background=True) API and the SSE resume protocol.

Background execution requires a db on the agent, team, or workflow. Submissions without one are refused with 400.

The AgentOS Control Plane submits every chat run with background=true. Runs started from the UI go through the same path as any other background submission: the concurrency cap applies, and the queue applies if your AgentOS is configured with QueueConfig(durable=True).

Without durability

BehaviorDetail
Bounded concurrencyAt most max_concurrency background runs execute at once per replica (default 32, or AGNO_BACKGROUND_MAX_CONCURRENCY).
WaitingRuns beyond the cap are accepted as PENDING, wait for a slot, and can be cancelled while waiting.
Resumable streamsEvents are buffered in-process. Reconnect through /resume.

The run lives in the memory of the replica that accepted it. If that process dies, every waiting and in-flight run on it is lost, and nothing marks them as failed. Without shared coordination, /resume and /cancel only work on the replica that holds the run.

agent_os = AgentOS(
    agents=[agent],
    db=db,
    queue=QueueConfig(max_concurrency=16),
)

With durability

QueueConfig(durable=True) writes each accepted run as a row in the queue table before the 202 is sent. A worker on every replica claims rows and executes them. If a replica dies, its runs are either reclaimed by another replica or marked failed, depending on max_attempts.

See Durable queue for the exact guarantee, retry policy, idempotency keys, and configuration.

With more than one replica

Set QueueConfig(redis=...) as soon as you run two or more replicas behind a load balancer. One setting installs both the event stream and the cancellation manager on each replica, backed by a shared Redis, so a run started on one replica can be watched, resumed, and cancelled from any other. No set_cancellation_manager() or set_event_stream() call is needed.

See Multi-replica deployments for what Redis does here, and why queue.redis is a different job from db=RedisDb.

Upgrading from v2

Background runs are capped at 32 per replica in v3. In v2 each submission spawned an unbounded asyncio.create_task; now runs beyond the cap wait as PENDING. Raise or disable the cap with QueueConfig(max_concurrency=...) or AGNO_BACKGROUND_MAX_CONCURRENCY. Durability, Redis coordination, idempotency keys, and the /queue endpoints are opt-in through QueueConfig.

Guides

Limitations

AreaLimit
Live stream viewBest-effort. The run row is authoritative. A disconnect is harmless and reconnecting replays events, but continuity across retry attempts is not guaranteed. Event indices are strictly increasing, not gapless. Termination is signalled by run status, not by index arithmetic.
Non-queueable submissionsMedia uploads, kwargs that plain JSON cannot store (for example an output_schema class), factory-backed components, and version-pinned lookups cannot ride the queue. They fall back to the bounded in-process path with a logged warning. Non-streaming background submissions still get a 202, but that response does not establish durable acceptance.
Queue storesPostgres (sync and async) and RedisDb. Any other db raises at startup. RedisCluster clients are rejected for the queue store.
Blocking workLease heartbeats run on a dedicated thread, so a sync model client or sync tool cannot starve its own lease. Blocking the event loop still delays cancellation checkpoints, timeout enforcement, and event publishing. Keep blocking work in threads.
Development loopThe queue behaves the same in development. A job accepted before a restart executes after it. Runs in flight at the default max_attempts=1 are failed with interrupted by worker shutdown once the drain window closes.

Developer Resources