Skip to content

Run classes ​

Every agent run belongs to one of two classes. The class decides which queue it waits in, how many runs of that class may be active, and how urgent it is. The same two classes exist on Console SaaS and on-prem installs.

defaultdeferred
Meant forInteractive and near-real-time work: chat, an API call someone waits for, a CI stepBatch-like work that can wait: nightly reports, backfills, bulk document analysis
How to askNothing (the default)"class": "deferred" on the runs API, service_tier: "flex" on the OpenAI endpoints, or class on a trigger
ExecutionInline (held connection) or queuedAlways queued, started when a slot frees
CapActive runs per tenant (default 250), counting inline, queued and runningRunning at once per tenant (default 2); queued per tenant (default 10 000)
When over the cap429 agent_run_concurrency_limit at onceQueued up to the queued cap, then 429
WaitingShortUp to AGENT_DEFERRED_MAX_WAIT_MS (24 h), then failed / expired
WorkersAGENT_RUN_CONCURRENCY per nodeAGENT_DEFERRED_CONCURRENCY per node (half of the default workers, at least 1)

Deferred runs never take interactive capacity: they have their own queue and their own workers, so a thousand queued nightly jobs cannot slow a chat down.

Asking for a class ​

bash
# Runs API
curl -X POST "$CONSOLE_URL/api/client/v1/agents/analyst/runs" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" -H "Content-Type: application/json" \
  -d '{ "input": "Analyse contract 17", "class": "deferred" }'

# OpenAI Responses
curl -X POST "$CONSOLE_URL/api/client/v1/responses" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" -H "Content-Type: application/json" \
  -d '{ "model": "agent:analyst", "input": "Analyse contract 17", "service_tier": "flex", "background": true }'

A deferred run is never answered inline. A request that waits (wait on the runs API, or an OpenAI call without background) polls the run for you up to the wait window and then answers 202, exactly as for any slow run. For deferred work, do not wait: take the run id and collect the result later through a callback or webhook, or by polling.

run_at and delay_seconds combine with a class: the run is scheduled, becomes due, and then waits for a slot like any queued run. The 24-hour limit counts from when the run became due, not from when you created it.

A trigger sets the class of every run it creates. A webhook trigger fed by a busy sender (every push, every pull request update) belongs in deferred, so a burst of events queues instead of using interactive capacity; the sender gets its 202 as soon as the run is queued either way.

Lifecycle ​

scheduled ──(due)──► queued ──(slot)──► running ──► succeeded | failed | canceled
                       │                   └──► requires_action (waits for a person)
                       └──(waited too long)──► failed, error.type "expired"
StatusMeaning
scheduledHas a start time in the future (scheduled_for). Counts for neither cap on the default class and holds no session lock.
queuedWaiting for a worker (default) or a deferred slot.
runningExecuting.
requires_actionPaused for an approval or a question. A person must answer; see approvals.
succeeded, failed, canceledTerminal. failed carries error.type: agent_error, max_duration_exceeded, worker_lost, expired, interrupted, …

A deferred or scheduled run that is not started within AGENT_DEFERRED_MAX_WAIT_MS after it became due fails with expired. A run that was running when the server shut down and could not finish within the drain time ends failed with interrupted, and is not retried automatically (it may already have had side effects): start it again yourself. Queued runs survive a restart and are published again at boot.

See the queue ​

bash
curl "$CONSOLE_URL/api/client/v1/agents/runs/queue" -H "Authorization: Bearer $COGNIPEER_API_TOKEN" | jq
json
{
  "object": "agent.run_queue",
  "default":  { "running": 3, "queued": 0, "scheduled": 1, "limit": 250 },
  "deferred": { "running": 2, "queued": 148, "scheduled": 12, "running_limit": 2, "queued_limit": 10000, "oldest_queued_at": 1760000000 },
  "project": { "default": { "running": 1, "queued": 0, "scheduled": 0 }, "deferred": { "running": 0, "queued": 20, "scheduled": 0 } }
}

The counts are tenant-wide; project repeats them for the token's project. Use it to decide whether to submit more, or to alert on a growing deferred backlog (oldest_queued_at). To list the waiting runs themselves: GET /agents/runs?status=queued,scheduled&class=deferred.

429 semantics ​

CodeClassMeaningWhat to do
agent_run_concurrency_limit (scope: tenant or project, class: default)defaultThe tenant (or project) already has the maximum number of active default-class runs. Counts inline, queued and running runs.Retry with backoff, or send the work as deferred.
agent_run_concurrency_limit (class: deferred)deferredThe tenant already has AGENT_DEFERRED_MAX_QUEUED_PER_TENANT queued or scheduled deferred runs.Slow the producer; watch the queue endpoint.
agent_quota_exceededanyThe agent-run quota is used up (Retry-After: 60).Wait, or raise the quota. A run counts a quota unit only once it is admitted: a call refused for a conflict or a cap costs none.
spend_limit_exceededanyThe tenant's spend limit is used up.Not a retry case; raise the limit or wait for the period to reset.

The error body names the limit and scope; the response is refused before a run is created, so a retry never duplicates work. A resume of a paused run (after an approval) is not refused by the cap: it continues work that was already accepted. agent_run_concurrency_limit carries Retry-After: 30. A stream is checked against the caps and the quota before it opens, so over a limit you get this plain JSON 429, not an error event; and the session that the refused call created is removed again.

A scheduled run is promoted to queued only while the tenant (or project) is under its default-class cap: at the cap it stays scheduled and starts when a slot frees up, so it can start later than its run_at.

Settings ​

VariableDefaultMeaning
AGENT_RUN_MAX_ACTIVE_PER_TENANT250Default class: queued + running runs of all kinds per tenant; 0 = no cap. The effective cap is the smaller of this and the tenant quota maxConcurrentAgentRuns.
AGENT_RUN_MAX_ACTIVE_PER_PROJECT0The same per project; 0 = off.
AGENT_RUN_CONCURRENCY4Default-class queue workers per node.
AGENT_DEFERRED_MAX_RUNNING_PER_TENANT2Deferred runs running at once, per tenant.
AGENT_DEFERRED_MAX_QUEUED_PER_TENANT10000Deferred runs queued or scheduled, per tenant.
AGENT_DEFERRED_CONCURRENCYhalf of AGENT_RUN_CONCURRENCY, at least 1Deferred queue workers per node.
AGENT_DEFERRED_MAX_WAIT_MS86400000 (24 h)A deferred or scheduled run not started this long after it became due fails with expired.
AGENT_BACKGROUND_MAX_DURATION_MS1800000 (30 min)Wall-clock ceiling of any run.
AGENT_SYNC_TIMEOUT_MS600000 (10 min)The wait window of a synchronous call (not a limit on the run).

The full list, with the callback, drain and trigger settings, is in Configuration.

Sizing guidance ​

  • A run is mostly waiting on a model. One node with AGENT_RUN_CONCURRENCY=4 is four turns started at once from the queue; inline calls (answered on the request that carried them) are bounded by the tenant cap, not by the workers. Raise the number of nodes, not just the concurrency, when CPU or memory is the limit.
  • Interactive tenants: keep the default cap generous (250 is a ceiling, not a target), and set AGENT_RUN_MAX_ACTIVE_PER_PROJECT if one project should not starve another. The effective cap never exceeds the tenant's own quota.
  • Batch tenants: leave AGENT_DEFERRED_MAX_RUNNING_PER_TENANT at 2 until you have measured. Each running deferred run holds a model call; the model provider's rate limit is usually the real ceiling. Raise it in small steps and watch 429s from the provider.
  • SaaS: the limits are per tenant, so one customer's backlog never touches another's. On-prem: you are the only tenant (or a few); the same settings protect your model providers and your database.
  • Time budgets: set the window (AGENT_SYNC_TIMEOUT_MS) below the shortest timeout of your load balancer or gateway, so a slow call becomes a clean 202 instead of a proxy error; the run keeps going either way.
  • Shutdown: on SIGTERM a node waits AGENT_RUN_DRAIN_TIMEOUT_MS (10 s by default) for its local runs, then marks the rest interrupted, before the HTTP server starts waiting for its open connections (a caller waiting on such a run gets 503 agent_run_interrupted, and open streams end). Give the pod a termination grace period larger than that, and make orchestrators drain nodes before stopping them.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub