Skip to content
Control Plane Labs

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/exports

What 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.

Other 2xx codes