durable=True, a background=true submission is written to the agno_jobs table before the 202 is returned. That job row is the acceptance. The run itself is stored in your sessions database as usual (the run row). A worker on every replica polls the jobs table, claims jobs, and executes them under a lease that it refreshes with heartbeats. The client contract is unchanged: 202 with run_id, then poll or stream.
The guarantee
Every accepted run reaches a terminal, visible outcome. Poll it and you will eventually seeCOMPLETED, ERROR, or CANCELLED. A run never stays RUNNING indefinitely and never disappears.
This is not the same as a promise that every run executes. What happens to a run whose worker dies mid-execution depends on max_attempts:
At the default, a crashed run is marked failed and an operator grants one more attempt through requeue. The run’s
content carries the reason:
TypeError in the call) go straight to failed regardless of remaining budget.
How it stays correct
Each claim increments the job’sattempt counter. That number is recorded on every write the attempt makes: the job row, the run row, and the event stream. A write carrying an older attempt than the one currently recorded is refused. This is what makes max_attempts > 1 safe: a worker that was swept while still alive cannot corrupt the retry’s output.
Queue retries and model retries
Model retries and queue retries are independent layers:
When model retries are exhausted, the run finishes with status
ERROR. The worker then consults the queue budget: with attempts remaining it requeues the job after a jittered delay, otherwise the job is failed. At the defaults, a model outage fails the run on the first error with no re-execution at either layer.
max_attempts=1 for runs with side effects. The two budgets multiply: at most max_attempts * (retries + 1) model calls per step.
Queue stores
The queue store defaults to the AgentOSdb. A dedicated store isolates queue polling from your session data.
db= requires durable=True. Passing a queue store without durability raises at construction.
Idempotency keys
Send anIdempotency-Key header to make a resubmission return the original run instead of enqueueing a second one:
Keys are scoped per user: the same key from two different
user_id values is two runs. Anonymous submissions share one namespace.
Asking is idempotent. Executing is not. A run that reached a tool with side effects has already acted, and a retry acts again. Agno does not keep a side-effect ledger. If a tool must not run twice, make the tool itself idempotent.
Session serialization
serialize_sessions=True (the default) allows at most one live run per session, executed in submission order. Two background=true submissions to the same session run one after the other instead of racing each other’s context reads and session-state writes. Different sessions still run concurrently under max_concurrency.
Set
serialize_sessions=False to restore fully concurrent claiming. Serialization applies at durable-queue claim time only. The non-durable in-process path is not session-gated.
Latency
A submission accepted on a replica wakes that replica’s worker as soon as the row commits, so execution starts in milliseconds.poll_interval (default 1.0 seconds) bounds three other things: how quickly a replica picks up jobs enqueued by other replicas, when a retry becomes claimable, and how often the sweep runs. Lowering it below the default rarely helps a single-replica deployment.
Configuration
The timing fields (
lock_grace_seconds, stop_timeout_seconds, retention_seconds, max_attempts, timeout_seconds) must be identical on every replica sharing a queue table. See Fleet-wide settings.