Async Jobs
How long-running MCP tool calls return immediately and let clients poll for completion.
Some MCP operations — today, only bulk_generate_access_codes — take longer than the 30-second budget a short-lived HTTP request can hold. Those tools return a job_id immediately and the client polls get_job_status for completion.
The pattern
- The client invokes a long-running tool — for example
bulk_generate_access_codeswith a batch of several hundred codes. - The server enqueues the work and returns immediately:
{
"data": {
"job_id": "0a4e7b96-c358-4d12-9f6b-25a8013ce74f",
"status": "queued",
"enqueued_at": "2026-05-18T10:05:00Z",
"plan_id": "c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b",
"quantity": 250,
"preview": {
"free_remaining": 120,
"chargeable_quantity": 130,
"will_bill_overage": true
}
}
}preview is there so an agent can tell a human what the batch will cost before
the work runs. See bulk_generate_access_codes.
- The client periodically calls
get_job_statuswith thejob_id:
{
"data": {
"job_id": "0a4e7b96-c358-4d12-9f6b-25a8013ce74f",
"tool_name": "bulk_generate_access_codes",
"status": "completed",
"result": { "count": 250 },
"error": null,
"started_at": "2026-05-18T10:05:01Z",
"completed_at": "2026-05-18T10:05:04Z"
}
}The envelope is the same for every async tool; result is whatever the tool
that enqueued the work returns, so read it against that tool's own page.
- When
statusbecomescompletedorfailed, the caller readsresultorerroraccordingly.
Every job reaches one of those two. A worker that gives up after its retries
records the failure on the row rather than leaving it queued, and the guard
clauses that abandon a batch early — a deleted plan, a project with no
access-code payment method, a deleted user — each say so in error.message.
Status values
| Status | Meaning |
|---|---|
queued | Job enqueued but not yet picked up by a worker. |
running | Worker started executing the job. |
completed | Job finished successfully. data.result carries the output. |
failed | Job failed. data.error carries a standard error envelope. |
Suggested polling cadence
- First 10 seconds: poll every second.
- Next 50 seconds: poll every 5 seconds.
- After 1 minute: poll every 30 seconds.
Jobs typically complete in well under a minute.
Persisting across sessions
Job IDs are stable. An agent that loses its conversation context can still poll get_job_status from a new session as long as the token owner matches — the job row is scoped to the token's team the same as any other read.
Which tools are async
Only bulk_generate_access_codes enqueues an async job today. Every other tool runs synchronously. The tool's description (surfaced at MCP handshake time) states whether the tool returns a job_id.
Related
bulk_generate_access_codes— the tool that queues a job.get_job_status— the polling tool.preview_access_code_cost— call this first to see whether a batch will trigger overage billing before you queue it.
How is this guide?