• Lesson 10 of 11
  • 6 min read
  • Lesson

Async & Long-Running REST Operations

Async and long-running REST operations: the 202 Accepted pattern, status polling, job resources, webhooks vs polling, and designing APIs for exports and batch jobs.

Why this, first. Real APIs can’t do everything inside one synchronous request — exports, transcodes, and batch jobs outlive the connection. Senior interviews probe the two async patterns (202 + polling and webhooks) and, crucially, their delivery guarantees. That’s where idempotency (Lesson 3) comes back: a webhook you can’t dedupe is a bug waiting to bill a customer twice.

The work outlives the requestso you make it a resourcereturn 202 & return nowthen poll or get a callback
ASYNC REQUEST–REPLY · 202 + A STATUS RESOURCE① POST /exportsclient submits work② 202 AcceptedLocation: /jobs/123 · queued, not done③ status resource/jobs/123 · trackable④ poll GET /jobs/123 → running… (honour Retry-After)⑤ 200 done: succeeded → link to result resourcesigned, expiring download URLClient
202 means "I've taken this, but it isn't done." Turn the work into a resource the client can track, then return immediately.3

The client polls the status resource with GET; it returns pending/running and, when finished, a done flag plus a link to the actual result resource. This is Google’s long-running Operation model: a uniform envelope carrying name, done, and either an error or a response. 1 Keep the operation queryable after completion so a client that crashed mid-poll can recover.

When synchronous stops fitting

Reach for async the moment the work outlives the request. Holding a connection open for minutes is a resource leak and a reliability trap — a dropped socket loses the client’s only handle on work that’s still running.

Long jobs Data exports, video transcode, batch recompute — minutes, not milliseconds.

Slow dependencies Third-party calls you don’t control and can’t speed up.

Timeout risk Anything that would blow a gateway or load-balancer timeout window.

The senior instinct Turn the work into a resource the client can track, then return now.

The job as a state machine

The status resource is just a small state machine the client reads on each poll. Model it explicitly — terminal states are the recovery story.

STATUS RESOURCE = A SMALL STATE MACHINEqueuedaccepted, waitingrunningworker picked it upsucceeded ✓→ result link · terminalfailed ✗→ error · terminal
One envelope, four states. done=true resolves to exactly one of succeeded (a response) or failed (an error) — never both, never neither.1

Polling vs. webhook callback

Polling makes the client chase you. A webhook inverts it: the client registers a callback URL, and the server POSTs an event when something happens. Efficient and push-based — but it hands the consumer a fistful of distributed-systems problems.

TWO SHAPES, TWO TRADE-OFFSPOLLING (202 + status)ClientServerGET /jobs/123running… (again, again)✓ dead simple · no client endpoint✗ wasteful · laggy (next tick, not now)Also: SSE / WebSockets / long-poll forlow-latency continuous updates.WEBHOOK (callback)ServerClientPOST event (signed)✓ efficient · near-real-time push✗ public endpoint · every delivery semanticat-least-once · out-of-order · mustverify auth · block replays · ack fast
Mature systems offer both: the webhook delivers the happy-path push; the pollable status resource is the fallback for missed deliveries and crash recovery. Belt-and-suspenders, not indecision.

Webhook delivery semantics

Naming these — and saying how you’d handle each — is the senior signal.

1 · Delivery is at-least-once The same event can arrive more than once. The consumer must be idempotent and dedupe on an event id — Lesson 3 cashing in. 2

2 · Ordering is not guaranteed Events can land out of order. Carry timestamps / sequence numbers; never assume the latest write is the last you received.

3 · Authenticity must be verified Anyone can POST a public URL. Sign the payload with an HMAC header (shared secret) so the receiver confirms it’s really you. 2

4 · Replay must be blocked A captured valid request can be resent. Put a timestamp in the signed payload and reject stale ones even when the signature checks out.

Ack fast, work later. Return a 2xx immediately, do the heavy work async. A non-2xx or timeout triggers retries with backoff; after enough give-ups, route to a dead-letter for inspection.

✗ Myth: “I’ve signed the webhook, so each event is genuine and safe to apply once.”

✓ Truth: the signature proves who sent it — not how often. You still need event-id dedupe and a replay window.

♻️ Memory rule: Outlives the request → make it a resource → 202 + status to poll, or a signed webhook to call back. Webhooks are at-least-once and out-of-order, so the consumer must be idempotent and time-aware — or it’s wrong.

Retrieval practice

Close the lesson in your mind first, then answer from memory. Effortful recall is what converts this from “I read it” to “I own it.” Immediate feedback below.

Q1. A server returns <code>202 Accepted</code> with a <code>Location</code> header. This signals that…
  • the resource was created and persisted at that path now
  • the work was accepted and a status resource is at that link
  • the client must resend the identical request once again soon
  • the request failed and a retry will be queued for later run
Q2. Webhook delivery is at-least-once, so the consumer must…
  • reply with a redirect status to confirm it got the event ok
  • trust ordering and apply each event strictly as it arrives in
  • be idempotent and dedupe repeats using a stable event id key
  • poll the sender first to verify the event is genuine and fresh
Q3. Why include a timestamp inside the signed webhook payload?
  • to let the consumer sort the events into their correct order
  • to record exactly when the consumer first received the event
  • to choose which retry backoff interval the sender should use
  • to reject stale replays of a captured but otherwise valid call
Q4. A webhook receiver should return a quick <code>2xx</code> ack and…
  • do the heavy processing asynchronously after acknowledging it
  • finish all the heavy work before it sends any response status
  • return a server error so the sender retries the call once more
  • hold the open connection until the downstream job has finished

Rehearse the answer out loud

Interviewer: "Design an API for generating a large data export that can take several minutes, and notify the client when it's ready." ~60 seconds, from memory (no scrolling up) — then reveal and compare. Don't read the model first.

Reveal model answer

“Don’t block the request. POST /exports returns 202 Acceptedwith a Location header to /exports/{id} — a status resource. The client polls that (pending → running → succeeded), honoring Retry-After; when done it links to a downloadable result resource, a signed, expiring URL.

I’d also offer a completion webhook: deliver a signed (HMAC) event POST. Because delivery is at-least-once, the consumer dedupes on the event id and stays idempotent; it doesn’t rely on ordering, and a timestamp lets it reject replays. The receiver acks with a fast 2xx and works async — non-2xx triggers retries with backoff, then a dead-letter. And I’d protect the result URL with auth and an expiry.”

Why this scores: it uses 202 + a status resource correctly and handles webhook delivery semantics — idempotent consumer, signing, retries — instead of assuming perfect delivery. Offering both push and poll, plus securing the result, is the senior signal.

I’m your teacher — ask me anything. Want to compare the Google Operation envelope with a hand-rolled jobs API? Curious how to verify a Stripe-style HMAC signature step by step? Want me to grade your rehearsal answer or fire harder follow-ups on dead-lettering and exactly-once myths? Just say so in the chat.

Primary source (read this next)

📖 “Long-running operations” — Google AIP-151: the cleanest spec for the 202-style operation model — the Operation resource, done, polling, and result linkage. For webhook delivery semantics in the wild, Stripe’s webhooks guide is the high-trust reference.

Interview — pick your bar

Answer out loud in ~60s, then reveal. Core = recall · Senior = trade-offs & failure modes · Staff = synthesis under ambiguity · System Design = open design round (a different axis, not a harder level).

What does 202 Accepted mean?

"I've accepted the request for processing, but it isn't done and may not even have started." It's deliberately non-committal about the outcome — unlike 201 Created, nothing is guaranteed to exist yet. The response points the client at where to check progress.

What does the Location header point to in a 202 response?

To the status/operation resource the client should poll (e.g. /jobs/{id} or /operations/{id}), not to the final result. Contrast with 201 Created, where Location points at the newly created resource itself.

What is a webhook, and how does it differ from polling?

A webhook inverts the direction: instead of the client polling, the client registers a callback URL and the server POSTs an event to it when something happens. It's push-based — efficient and near-real-time — but it makes the client a server and hands it a fistful of distributed-systems problems (duplicates, ordering, auth).

What delivery guarantee do webhooks give, and what follows from it?

Delivery is at-least-once: failed deliveries are retried, so the same event can arrive more than once. The direct consequence: the consumer must be idempotent and dedupe on a stable event id. Assuming exactly-once delivery is the classic bug — it's how you bill a customer twice.

What's the core mental shift when moving from sync to async design?

Turn the work into a resource. Instead of returning the result, you create a first-class operation/job resource the client can address, query, and recover against. The request returns fast with a handle to that resource; the result becomes a separate resource the operation links to once it's done.

Why keep the operation queryable after it has completed?

So a client that crashed or lost the network mid-poll can recover — it re-queries the operation id and learns the outcome and result link. If you delete the operation the instant it finishes, you create a race where the client never learns it succeeded. Retain completed operations for a sensible TTL.

Webhooks aren't ordered. How do you build a correct consumer anyway?

Carry a timestamp and/or sequence number in each event and never assume the last event you received is the latest write. Reconcile against current state (e.g. "ignore an update older than what I've already applied"), or fetch authoritative state from a status resource. Design so out-of-order arrival is merely handled, not fatal.

A valid signed request can be captured and resent. How do you stop replay?

Include a timestamp in the signed payload and reject stale ones (outside a small tolerance window, e.g. a few minutes). A replayed event then fails even though its signature is genuine, because it's too old. Combine with consumer-side dedupe on event id so a replay inside the window is also a no-op.

"Just give me exactly-once delivery." How do you respond?

Exactly-once delivery is effectively unachievable over an unreliable network (the two-generals problem). What you can get is exactly-once effect: at-least-once delivery plus an idempotent consumer keyed on the event id. The senior framing is "I want exactly-once effect, not exactly-once delivery" — and then you build dedupe rather than chase an impossible guarantee.

How do idempotency keys interact with the 202 submission?

The submitting POST is not idempotent, so a retried submission could spawn a duplicate job. Accept an Idempotency-Key: on a repeat, return the same 202 and Location for the already-created operation instead of starting a second one. That makes "submit a long job" safely retryable — the client gets one operation no matter how many times the request is replayed.

How do you keep one slow or failing consumer from degrading webhook delivery for everyone?

Isolate per-consumer: independent delivery queues, per-endpoint concurrency limits and circuit breakers so a timing-out endpoint backs off without starving others; bound and parallelize retries; and dead-letter aggressively rather than letting a black-hole endpoint consume the retry budget. The principle is bulkheading — failure of one tenant's endpoint must not become a shared-fate outage of the delivery pipeline.

Async / long-running design framework — drive any "design a long-running operation" prompt through these, out loud:
  1. Clarify: expected duration, completion SLA, who the client is (can it receive a callback?), idempotency & consistency needs.
  2. Make the work a resource: POST → 202 Accepted + Location to a status/operation resource; keep it queryable after completion (TTL) for crash recovery.
  3. Model the operation as a state machine: queued → running → succeeded | failed | cancelled; done=true resolves to exactly one of result or error; link the result as its own resource.
  4. Choose the notification axis: polling (Retry-After + client backoff, works behind firewalls) vs signed webhooks (push, near-real-time) — mature APIs offer both, poll as the fallback.
  5. Make submission safe: Idempotency-Key so a retried submit returns the same operation, not a duplicate job.
  6. Webhook delivery semantics: at-least-once & unordered → consumer dedupes on event id, stays idempotent; HMAC-sign + reject stale (replay window); ack fast 2xx then work async.
  7. Reliability & scale: retries with backoff + jitter → dead-letter; per-consumer isolation/bulkheading; observability on delivery, retry, and lag.
Design an API for a large data export that can take several minutes, then notifies the client when it's ready.

A strong answer covers: don't block — POST /exports → 202 Accepted + Location to /exports/{id}, a status resource → client polls (pending→running→succeeded) honoring Retry-After; on done it links to a result resource — a signed, expiring download URL → also offer a completion webhook: an HMAC-signed event POST → because delivery is at-least-once, the consumer dedupes on event id and stays idempotent, doesn't rely on ordering, and uses a timestamp to reject replays → receiver acks fast 2xx and works async; non-2xx → retries with backoff → dead-letter → protect the result URL with auth + expiry → keep the operation queryable after completion so a client that crashed mid-poll can recover.

Design a webhook delivery platform that guarantees every event eventually lands without one bad endpoint taking the system down.

A strong answer covers: persist every event durably first, then deliver from a queue — never couple producing the event to delivering it → delivery is at-least-once: sign each payload (HMAC over raw bytes, constant-time compare) with a timestamp so consumers verify authenticity and reject replays → on non-2xx/timeout, retry with exponential backoff + jitter over a bounded window, then dead-letter and surface the failing endpoint → give a webhook-independent recovery path: a list/status API to fetch the gap and a replay of dead-lettered events → isolate per consumer (independent queues, per-endpoint concurrency limits, circuit breakers) so one black-hole endpoint can't starve the fleet — bulkheading → coalesce at high fan-out, apply back-pressure, and track delivery/retry/lag per endpoint → name the trade-off: exactly-once delivery is impossible over an unreliable network, so you ship at-least-once delivery + an idempotent consumer keyed on event id (exactly-once effect).