202 Accepted
Success response defined by RFC 9110 §15.3.3.
Last updated July 27, 2026
Common causes at a glance
- A long-running job accepted onto a background queue
- A batch or bulk endpoint that processes records out of band
- A webhook receiver acknowledging delivery before doing any work
- An API returning 202 for work it actually completed synchronously
Reproduce it
curl -i -X POST -H 'Content-Type: application/json' -d '{"report":"q3"}' https://api.example.com/v1/exportsWhat 202 Accepted means
202 exists because some work does not fit inside a request. Rendering a video, generating a quarterly export, fanning out a bulk email: none should hold a socket open for ten minutes. The server queues a job, returns 202 immediately, and the client comes back later.
HTTP has no way to deliver the eventual status of an asynchronous operation, so a 202 is deliberately noncommittal: the work might be rejected when a worker picks it up. Anything built on 202 must assume the job can still fail, which makes the polling resource, not the 202, the source of truth.
What the spec says
RFC 9110 §15.3.3 is unusually blunt: the request
“might or might not eventually be acted upon”, and there is no facility in HTTP for
re-sending a status code from an asynchronous operation. The response representation
ought to describe the request’s current status and point to or embed a status monitor.
No header is mandated for that, so practice is split between Location
(§10.2.2), Content-Location, and a URL in the body.
Retry-After (§10.2.3) tells pollers how long to
wait. Contrast 201, which asserts the resource exists now.
What actually causes it
The failure mode is almost always the polling contract, not the 202. A 202 with no status URL leaves the caller to invent one, usually by polling the collection endpoint in a tight loop. A status URL that 404s until the worker starts is worse: the client cannot distinguish “not started” from “bad job id” and gives up. Then there is the job that finishes before the first poll and has its status resource garbage-collected immediately, so a successful export looks lost. Plenty of services also return 202 for work completed inline, forcing callers to write polling code for nothing. If the work is done when you respond, send 200 or 201.
How to debug it
Submit the job, capture whatever URL comes back, then watch the state transitions rather than the 202:
curl -sD - -X POST -H 'Content-Type: application/json' -d '{"report":"q3"}' \
https://api.example.com/v1/exports -o body.json; cat body.json
Look for Location, Content-Location, or a
status_url field, and for Retry-After. Poll that URL and
confirm three things: it exists immediately, it reports terminal failure and not just
success, and it survives long enough after completion for a slow client to see the
result. Cap polling with a deadline and backoff. When chasing lost jobs, correlate the
job id against worker logs; the gap is nearly always between enqueue and the worker’s
first heartbeat.
Working examples
curl
# Submit, then poll. -D - prints headers so you can see Location/Retry-After.
curl -i -X POST \
-H 'Content-Type: application/json' \
-d '{"report":"q3","format":"csv"}' \
https://api.example.com/v1/exports
# HTTP/2 202
# location: https://api.example.com/v1/exports/7f3a/status
# retry-after: 5
curl -s https://api.example.com/v1/exports/7f3a/status
Python (requests)
import time
import requests
resp = requests.post(
"https://api.example.com/v1/exports",
json={"report": "q3", "format": "csv"},
timeout=10,
)
assert resp.status_code == 202
status_url = resp.headers.get("Location") or resp.json()["status_url"]
delay = float(resp.headers.get("Retry-After", 2))
deadline = time.time() + 300
while time.time() < deadline:
job = requests.get(status_url, timeout=10).json()
if job["state"] in ("succeeded", "failed"):
print(job["state"], job.get("result_url"))
break
time.sleep(delay)
delay = min(delay * 2, 30) # back off; do not hammer the queue
else:
raise TimeoutError("job never reached a terminal state")
Node (fetch)
const res = await fetch("https://api.example.com/v1/exports", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ report: "q3", format: "csv" }),
});
console.log(res.status); // 202
const statusUrl = res.headers.get("location") ?? (await res.clone().json()).status_url;
let delay = Number(res.headers.get("retry-after") ?? 2) * 1000;
for (const deadline = Date.now() + 300_000; Date.now() < deadline; ) {
const job = await (await fetch(statusUrl)).json();
if (job.state === "succeeded" || job.state === "failed") {
console.log(job.state, job.result_url);
break;
}
await new Promise((r) => setTimeout(r, delay));
delay = Math.min(delay * 2, 30_000);
}Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.