The problem
Some requests start work that takes minutes: rendering a report, exporting a year of orders, transcoding a video, provisioning an environment. Over a plain request and response, the client has to keep its connection open for all of that time, and every hop on the way has its own idea of how long a request may take. A few published limits, as examples (checked in October 2026):
- Amazon API Gateway gives a REST API integration between 50 ms and 29 s by default. Regional and private APIs can ask for more, which may lower the account's throttle quota; HTTP APIs stop at 30 s.
- Heroku's router ends a request whose response hasn't started within 30 s, and the limit can't be changed.
- Google Cloud load balancers use a backend service timeout of 30 s unless you set another.
- Cloudflare waits 125 s for the origin by default and then answers with error 524.
- Azure Functions: an HTTP-triggered function must answer within 230 s, whatever its own timeout says, because of the idle timeout of the Azure Load Balancer in front of it.
Browsers, HTTP client libraries, corporate proxies and mobile networks add limits of their own, and the shortest one on the path wins. When it fires, the gateway answers 504 Gateway Timeout, but the server behind it usually carries on: Heroku's documentation points out that the application isn't told about the timeout and keeps working on the request. Its result is then thrown away.
That is only the first cost:
- Held resources. Every waiting request occupies a connection on each hop and server capacity (memory, CPU, often a thread or a database connection) for the whole duration. A few slow endpoints can use up a pool that thousands of fast requests share, and every deployment has to wait for them or cut them off.
- Retries that duplicate work. A timeout tells the client nothing about whether the work happened. It retries, and the server starts the same report again while the first copy is still running. For an export that wastes capacity; for an order or a payment it does the thing twice.
- No progress and no way out. The client learns the outcome at the end or not at all, and it can't cancel work it no longer needs.
Raising every timeout to three minutes doesn't fix this. Some limits can't be raised, they differ from hop to hop, and a connection held for minutes still breaks when a phone goes to sleep or a server is redeployed.
How it works
Split the one long call into a short call that starts the work and a series of short calls that ask how it is going.
- Start.
POST /reportsreaches the API, which validates the request (a bad one still gets its400while the client is listening), stores a job record in statequeued, puts the job on a durable queue and answers at once with202 Accepted. The response carries the job's status URL inLocation, a polling hint inRetry-Afterand the job itself in the body. - Work. A worker takes the job from the queue, records
runningand its progress in the job store as it goes, writes the finished report to object storage, and only then marks the jobsucceeded, orfailedwith a reason. - Poll. After the suggested delay the client sends
GET /jobs/42. While the job runs, the answer is200with its state and progress, read from the job record, so a poll never waits for the work. Once the job has succeeded, the status resource answers303 See OtherwithLocation: /reports/42, and the client fetches the report from there.
Every request is now short. The long work runs in workers that can be scaled, retried and deployed on their own schedule.
The contract, checked against RFC 9110
| Element | What RFC 9110 says | How the pattern uses it |
|---|---|---|
202 Accepted |
The request has been accepted for processing but isn't finished, and might still be refused when it is processed. The code is deliberately noncommittal, and HTTP has no way to send another status code when the work ends. The response body ought to describe the current status and link to (or embed) a status monitor. | Send it once the job is durably recorded and queued, not before. Put the job, with its status URL, in the body. |
Location |
Its meaning depends on the method and the status code. RFC 9110 defines it for 201 Created (the new resource) and for redirects (the target). |
On a 202 the meaning is a convention, not a rule: Azure's pattern and Durable Functions use Location, while the Azure REST API guidelines use an Operation-Location header. Document which one you send, and repeat the link in the body. |
Retry-After |
How long the client ought to wait before its follow-up request, as a number of seconds or an HTTP date. | The polling interval. Send it with the 202 and with every status answer that isn't final. |
303 See Other |
A redirect to a different resource that serves as an indirect answer; the client retrieves it with GET (or HEAD), whatever method it used. A 302 Found lets the client change POST to GET or keep it, and 307 always keeps it. |
The finished status resource redirects to the result. Azure's guidance recommends 303 for exactly this reason: a client that replays its original method on a 302 could send a POST again. |
504 Gateway Timeout |
A gateway or proxy didn't get a timely answer from the server behind it. | What the synchronous version runs into. |
A redirect isn't the only way to finish. The status resource can also answer 200 with "state": "succeeded" and a link to the result, which the client then follows itself. That keeps the final status readable, which matters when many HTTP clients follow a 303 automatically and the code that polls never sees the status body. Pick one and document it.
The status resource
- States.
queued, thenrunning, then exactly one final state:succeeded,failedorcancelled. A final state never changes, and every job must reach one, because that is what tells a client to stop polling. Names vary between platforms (see below); what matters is a small, documented set. - Fields. The ID, the state, when the job was created and last updated, progress when the worker can estimate it, a link to the result once it has succeeded, and an error once it has failed. RFC 9457 problem details are a good shape for the error. The last-updated time also tells a client whether a slow job is still alive.
- A failed job is still a successful poll. Answer
200with"state": "failed"and the error, so the client can tell a job that failed from a status service that is down. The status monitor in the Azure REST API guidelines works this way. - No job may hang. A worker that crashes leaves its job
runningforever unless something notices. Let workers refresh the last-updated time as they go, run a sweeper that fails jobs that stay silent too long or pass a deadline, and cap the number of attempts, so that every job ends in a final state. - Who may read it. A job's status and result are as sensitive as the request that started it. Check on every poll and every download that the caller may see this job; knowing the URL is not a permission. Use IDs that can't be guessed, such as random UUIDs (the diagram's
42is only for readability). OWASP treats random identifiers as defence in depth that never replaces the access check. If downloads should bypass your API, hand out a short-lived signed URL to the object store; Azure's pattern mentions shared access signatures for this, which is the Valet Key pattern. - Keep caches out of it. The answer changes from poll to poll, so send
Cache-Control: no-store, and no browser or proxy cache can answer a poll with an old state. Tell clients how long a job is kept in the body (anexpiresAtfield, say) and in your documentation. Azure's pattern suggests anExpiresheader for this, but RFC 9111 definesExpiresas the moment a stored response becomes stale and states that it says nothing about when the resource itself will go away.
When to use it
- Work that regularly takes longer than the shortest timeout on the path, or comes close to it: reports, exports and imports, media processing, batch inference, provisioning.
- Clients that can't receive calls, such as browsers, mobile apps and scripts behind NAT or a firewall. Polling needs only outgoing requests.
- When the client needs progress, a way to cancel, or an outcome that it can still read after a disconnect.
- When work should be accepted even while the back end is busy or being deployed. The queue holds it until a worker is free.
- Not for work that finishes well within the timeouts. Answer it synchronously: one request is simpler, faster and easier to debug. As thresholds, the Azure REST API guidelines make an operation long-running when its 99th-percentile time is above 1 s, and Google's AIP-151 suggests 10 s as a rule of thumb.
- Not when results should reach the client while they are produced (stream them with server-sent events or WebSockets), or when the client already has an event channel, such as a webhook endpoint or a message broker.
Trade-offs
- More moving parts. A job store, a queue, workers, result storage, a status endpoint and clean-up jobs replace one request handler.
- Polling costs and delay. The client learns about the end up to one interval late, and every client polls. At one
GETevery 5 seconds, a 2-minute job costs about 24 polls, and 1,000 such jobs at once add 200 requests per second. Keep each poll cheap (one key lookup, no joins) and tuneRetry-After. - A contract clients must follow. They have to handle the
202, find the status URL, honourRetry-After, stop on a final state and follow the redirect. Platform SDKs and integration tools often do this for them, which is a good reason to follow an existing convention instead of inventing one. - At-least-once execution. Queues deliver a job again when a worker crashes or its lease runs out, so the same job can start twice. Workers must be idempotent: see Idempotent Consumer.
- State to keep and to delete. Job records, results and idempotency keys pile up unless they expire.
- Harder to follow. One operation spans many requests and processes. Put the job ID in every log line and trace.
Implementation notes
Polling intervals and backoff. Honour
Retry-Afterwhen the server sends it. That lets the server spread the load and lengthen the interval for long jobs, for example to a fraction of the estimated time left. Without a hint, start short and back off (1 s, 2 s, 4 s and so on up to a cap), add jitter so that clients started together don't poll in lockstep, and give up after an overall deadline. Treat429or503with aRetry-Afteras a request to slow down, not as a failed job. It is the advice of Retry with Backoff & Jitter, applied to polls, and a client still needs its own deadline for the whole operation, as in Timeout & Fallback.Idempotency for the initial request.
POSTisn't idempotent, and its202can be lost like any response, so the client can't tell "no job" from "job created, answer lost". Let it send anIdempotency-Keyheader with a random value, store the key with the job (unique per client), and answer a repeat with the original202and the same job, so that nothing is queued twice. Azure's pattern recommends exactly this. The IETF draft for the header (draft-ietf-httpapi-idempotency-key-header) reached version 07 in October 2025 and expired in April 2026 without becoming an RFC, but its rules make a sound default. The value is a Structured Fields string, ideally a UUID. A repeat that arrives after the first request has finished gets the first result, a repeat while the first is still being processed gets409 Conflict, the same key with a different body gets422, and a missing key where one is required gets400. The server publishes how long it keeps keys; Stripe's API, which works this way, may drop keys once they are 24 hours old. The Azure REST API guidelines reach the same goal with a client-chosenOperation-Idheader and a409when an ID is reused for a different request. Inside the worker, the same idea protects against redelivered messages: Idempotent Consumer.Where the work happens. A durable queue between the API and the workers keeps accepted jobs safe while workers are busy, crash or are redeployed, and lets you scale the workers on the backlog: see Queue-Based Load Leveling and Competing Consumers. Web-Queue-Worker is the application style built on this split. Two details matter. Saving the job record and enqueueing the job are two writes, and if the enqueue fails after the save, the job stays
queuedforever: write both through a Transactional Outbox, or let a sweeper re-enqueue or fail old queued jobs. And store the result before marking the jobsucceeded, so the status never points at a file that isn't there yet. Serverless functions triggered by the queue make good workers for shorter jobs, within hard limits: an AWS Lambda function runs for at most 15 minutes (90 when queue-triggered on Lambda Managed Instances), and a function on the Azure Functions Consumption plan for at most 10 (5 by default). Longer jobs need checkpoints, a split into steps, an orchestrator such as Durable Functions, or containers.Notifications instead of polling. If the client can take calls, a webhook to a URL it registered, for example in the
POST, replaces the polling. Browsers can't take calls, but they can keep a connection open: server-sent events stream status updates from server to client over HTTP, WebSockets carry messages both ways, and long polling (RFC 6202) holds each poll open until something changes. Keep the status resource in every case: notifications get lost, and a client that reconnects needs a place to catch up.Cancellation. Azure's pattern cancels with
DELETEon the status URL, as the diagram does. The Azure REST API guidelines model cancel as an action (POSTon the status monitor's URL with:cancelappended). Google's Operations service has a best-effortCancelOperation, after which the operation ends with aCANCELLEDerror, and a separateDeleteOperationthat only says the client no longer wants the result and cancels nothing. Pick one model and document it. Cancelling is cooperative: record the request, let the worker check for it between steps and stop, then set the state tocancelled. Decide what happens to partial work (keep it, undo it, or run a Compensating Transaction), and reject a cancel for a job that has already finished, for example with409 Conflict.Clean-up. Records and results cost storage, so give them a retention period and publish it. The Azure REST API guidelines keep a status monitor for a documented period of at least 24 hours after the operation completes; AIP-151 suggests 30 days as a rule of thumb for expiring operations. Expire result files with the object store's lifecycle rules, prune idempotency keys on the same schedule, and answer polls for expired jobs with
404 Not Foundor410 Gone; RFC 9110 reserves410for resources that are gone for good.How platforms model long-running operations (examples, checked in October 2026):
- Azure REST API guidelines. A long-running action (a
POST) answers202with anOperation-Locationheader that holds the absolute URL of a status monitor, and does so even when the work has already finished; aPUTthat creates a resource answers201with the same header. The monitor has anid, astatus(NotStarted,Running,Succeeded,FailedorCanceled), anerrorwhen it failed and, for actions, aresult. Polls get200plusretry-afteruntil the status is final, and control actions such as cancel arePOSTrequests on the monitor. - Google AIP-151. A method that can take long returns a
google.longrunning.Operation, a resource that works like a promise: aname, adoneflag,metadatafor progress, and either aresponseor anerror. APIs don't invent their own interfaces; they implement the shared Operations service withGetOperation,ListOperations,CancelOperation,DeleteOperationandWaitOperation. - Azure Durable Functions. An orchestration started over HTTP answers
202withLocation(the status query URL),Retry-After: 10and a body of management URLs for status, raising events, terminating and purging history. The status URL keeps answering202while the orchestration runs and200once it has completed or failed. In .NET, an orchestrator's HTTP calls can follow this protocol automatically, and Azure Logic Apps' HTTP actions understand it too.
The three disagree on details:
LocationorOperation-Location,200or202while the job runs,DELETEor a cancel action. None of them is wrong. Choose one convention, document it, and keep to it across your APIs.- Azure REST API guidelines. A long-running action (a