skip to content

A batch endpoint processes 100 items and 3 of them fail validation. How do you report that over HTTP - what does status 207 Multi-Status mean, and when would you return a plain 200 with a per-item status array instead?

level: seniorimportance: must knowfreq 50%

answer

  1. one status line, many outcomes = must be per item
  2. 200 hides failures; 400/500 causes full replay of successes
  3. 207 Multi-Status = 'read the body, outcomes differ' (WebDAV)
  4. per item: correlation id + status code + machine-readable error
  5. whole-batch 4xx only for bad request: malformed, oversized, 401/403/429

basics

~20 s

Never a bare 200 or a bare 400 - both lie. Return one overall status plus a per-item result array where each entry carries its own status and error. 207 Multi-Status signals mixed outcomes explicitly; a plain 200 with the same array is the pragmatic, better-supported alternative.

solid answer

~60 s

The transport succeeded and the outcomes are mixed, so no single code tells the truth. The contract must be **per item**. Return a result array where each entry has the caller's correlation id, a per-item status code and, on failure, a machine-readable error. The envelope status is then a summary: - **207 Multi-Status** (from WebDAV, RFC 4918) means exactly "read the body, outcomes differ per part". It is the semantically honest code and it stops clients treating 200 as blanket success. - **200 OK** with the same body is what most large APIs actually ship, because 207 is unfamiliar to SDK generators, some gateways, and dashboards that bucket anything non-2xx-standard oddly. What matters more than the number is that clients cannot succeed by checking the envelope alone. I would return 207 when consumers are in-house or the docs can carry the burden, and 200 for a wide public audience - and in both cases include counts (`succeeded`, `failed`) so log-level triage does not require parsing 100 entries. A whole-batch 4xx is correct only when the *request* is bad: malformed JSON, over the item limit, unauthorised.

code

http · 13 lines
http
HTTP/1.1 207 Multi-Status
Content-Type: application/json

{
  "succeeded": 2,
  "failed": 1,
  "results": [
    { "clientRef": "a1", "status": 201, "id": "ord_881" },
    { "clientRef": "a2", "status": 422,
      "error": { "code": "SKU_UNKNOWN", "field": "sku" } },
    { "clientRef": "a3", "status": 201, "id": "ord_882" }
  ]
}

go deeper

for a junior

Say that a single status code cannot describe mixed outcomes, so the response body must carry a result per item.

for a middle

Name 207 Multi-Status and its meaning, and describe the per-item result shape including correlation id and per-item status.

for a senior

Argue the 207-versus-200 tradeoff from the client population, separate request-level failures from item-level failures, and specify retry guidance per item.

for a principal

Treat it as a contract-design and blast-radius question: what clients will do by default on each shape, how quotas and error budgets are affected, and whether async job submission with a results resource is the better shape at scale.

## The problem HTTP's status line describes the outcome of *one* request. A batch has many outcomes. Any single code you pick is either a lie or a loss of information: - **200 OK** implies everything worked. Clients written the obvious way (`if (res.ok) markAllSynced()`) will silently drop the 3 failures. This is the most common and most damaging bug in batch integrations. - **400 Bad Request** implies nothing worked. The caller retries all 100, duplicating the 97 that succeeded. - **500** is worse still: it invites a blind retry of everything and pollutes error budgets with a request the server actually handled. So the outcome has to live in the body, per item, and the status line becomes a summary. ## 207 Multi-Status 207 comes from WebDAV (RFC 4918) and means precisely: the response body contains multiple status codes, one per sub-operation, and the recipient must inspect them. It is defined for a WebDAV XML body but the code itself is widely reused for JSON batch responses. Its value is behavioural, not decorative. A client generated from a spec, or a naive `response.ok` check, treats 207 as success in some HTTP libraries too - but it is at least unfamiliar enough to prompt someone to read the docs, and it lets dashboards and gateway rules distinguish "mixed" from "clean". Use it when your consumers are internal, or when you can be confident the documentation will actually be read. ## When plain 200 is the better contract Large public APIs frequently return 200 with a result array. The reasons are practical: unfamiliar codes trip up generated SDKs, some proxies and API gateways have opinions about non-standard 2xx codes, and observability tooling buckets them awkwardly. If your audience is broad and heterogeneous, 200 plus a rigorously documented, machine-readable result array is defensible - the per-item structure is doing the real work either way. The decision is about **client population**, not correctness purism. ## Shape of the per-item result Each entry needs: 1. **The caller's correlation id** - never rely on array position, which breaks under reordering or omission. 2. **A per-item status**, typically the HTTP code that a single-item request would have returned: 201 created, 200 updated, 409 conflict, 422 validation failure, 404 not found. Reusing the code space means callers already know the vocabulary. 3. **A machine-readable error** on failure - a stable `code` plus a human `message`, and ideally a field pointer. RFC 9457 problem details is a good per-item shape. 4. **The created resource's id or URL** on success, so the caller does not need a follow-up read. Add envelope-level counts (`succeeded`, `failed`) so operators can triage from a log line without parsing every entry. ## What genuinely deserves a whole-batch error status Distinguish *request* failures from *item* failures. The request itself is bad - and a single 4xx is correct - when the JSON is malformed (400), the item count or body size exceeds the published limit (413 or 400), authentication or authorisation fails (401/403), or rate limits are exceeded (429). In those cases nothing was attempted and the caller should retry the whole thing after fixing the request. ## Retry guidance is part of the contract A partial-failure response must let the client build the retry set: retry only the items whose per-item status is retryable (429, 503, or a documented transient code), and never retry 422 validation failures unchanged. Say this explicitly in the documentation, because the default client behaviour - retry everything or retry nothing - is wrong in both directions.

  • A client times out on that batch request and retries the whole thing. What have you built if items are not idempotent?
    Duplicates for every item that actually succeeded before the timeout. The fix is per-item deduplication on a caller-supplied key, or a request-level idempotency mechanism that returns the original stored result for a repeated batch. Without one of those, a non-idempotent POST batch plus any retry policy guarantees duplicates under network failure.
  • Should the per-item status values be HTTP status codes or your own error enum?
    Reuse HTTP codes for the outcome (201, 409, 422) because clients already know the vocabulary and can map retryability from it, and add your own stable string `code` inside the error object for the specific reason. That gives coarse standard semantics plus precise domain detail without inventing a second status vocabulary.

saying these in an interview costs you the question

  • Returning 200 OK with failures buried in the body and no documented per-item contract
  • Returning 400 or 500 for the whole batch when most items succeeded
  • Believing 207 is part of the core HTTP specification's general status registry rather than originating in WebDAV
  • Correlating results by array index instead of a client-supplied reference
  • Telling clients to retry the entire batch on any failure, duplicating the successes

context