Partial Success Response
also called Per-Item Outcome, Multi-Status Response
A batch response that reports the outcome of every item individually - so a caller can retry exactly the failures, rather than inferring from one status code what happened to five hundred items.
A batch endpoint accepts 500 items. Two fail validation and 498 are applied. No single HTTP status code describes that, and each of the obvious shortcuts moves work onto the client that the client will get wrong.
Return 200 with a success body and the caller's happy path treats the batch as complete, so the two failures are discovered weeks later by a reconciliation, or never. A partial success reported as a success is a data-loss mechanism, and it is the most common error-design mistake in batch APIs. Return 400 for the whole batch and the caller resubmits 500 items to retry 2 - which, without per-item idempotency, writes 498 duplicates. Return 500 and you have asked a client to retry work that succeeded while blaming yourself for the caller's input.
The pattern is to report per item: a status per element, keyed by an identifier the client supplied, with machine-readable error details, under a documented top-level status - 207, or 200 with the shape specified.
Why it matters
Batch endpoints exist because per-item calls are too slow or too expensive, and the case that dominates them in production is neither total success nor total failure. Designing only for the two clean outcomes guarantees that the common one is handled by accident.
The second reason is that this shape is what makes retry correct. A client that can identify its two failed items, and resubmit only those safely, needs nothing else. A client that cannot do either has two options - give up, or resubmit everything - and both are worse than the response shape being right.
Implementation patterns
- Client-supplied item identifiers, echoed in each result. Position-based matching breaks the moment the server reorders, filters or deduplicates, and it breaks silently.
- Per-item idempotency keyed on that identifier, so the retry of a subset cannot duplicate the successes. This is the non-negotiable half of the pattern.
- Conventional error structure per item - RFC 9457 (2023), which replaced RFC 7807, defines one
- with a stable machine-readable type rather than prose.
- A summary count of succeeded and failed, so a client can assert on a number rather than walking the array to decide whether anything went wrong.
- An explicit cap on batch size, documented and enforced with a clear error, and sized so the batch completes inside roughly 30 seconds of server time. An unbounded batch is a timeout waiting to happen, and a timeout on a batch is the worst case of all - an unknown outcome for every item.
- State the atomicity in the contract. Either the batch is all-or-nothing, in which case one failure means a 4xx and no side effects, or it is per-item. Accidental partial atomicity - some items applied, the response silent about which - is the failure to design out.
Industry example
HTTP has carried a multi-status response (207) since the WebDAV specification, and the pattern is visible across large public APIs: bulk endpoints at payment, messaging and advertising platforms return per-item outcomes with caller-supplied references, precisely so that a partner's retry is scoped to the failures.
The instructive counter-example is the internal batch endpoint that returns 200 and an array of error strings. It passes every test, satisfies the integration, and produces a reconciliation discrepancy a quarter later - and because the errors were prose, nobody can categorise the failures without reading them one by one. The lesson is that the shape has to be designed before the first consumer, because afterwards it is a breaking change.
Failure scenarios
- Partial success reported as success, the headline failure: silent data loss discovered by reconciliation.
- Position-based result matching, which maps outcomes to the wrong input records after the server filters duplicates.
- A retried subset duplicating the successes, because per-item idempotency was never implemented.
- A batch timeout, leaving every item in an unknown state with no way to query what was applied - which is why a companion "status of batch" endpoint is worth having for long batches.
- Prose errors, which cannot be counted, alerted on or routed, so the client's error handling degenerates into string matching.
Trade-offs
| Choose | Gains | Pays |
|---|---|---|
| Per-item outcomes | precise retries - no duplicate writes - countable errors | a richer contract - per-item idempotency to implement |
| All-or-nothing batch | trivial client logic - clean semantics | one bad item wastes the whole batch - poor fit for validation errors |
| Single items only | simplest possible contract | round-trip cost per item - rate limits reached sooner |
The honest reading: per-item outcomes push complexity into the contract to keep it out of every client. With one consumer that is a poor trade; with twenty it is obviously right, and the contract is written once.
When not to use it
For small batches - two or three items - make it atomic and return a 4xx. Per-item machinery is more contract than the problem needs, and the wasted work on a retry is negligible.
For interdependent items, partial success is not a shape to report but a state to prevent. A double-entry accounting posting, a transfer between two accounts, or a set of records that are only meaningful together must be one transaction, and the correct response to one bad item is to reject the whole request. Reporting a partial application there would be reporting a corrupted state politely.
Interview question
Q: Design the response for an endpoint that accepts up to 1,000 records. Tell me what the client must be able to do with it, and what you would need to change in the service to make that possible.
What a strong answer covers: per-item outcomes keyed by a client-supplied reference, not by position; a documented top-level status and a summary count; machine-readable per-item error types using a conventional format; per-item idempotency as the prerequisite for a safe partial retry; an enforced batch-size cap and a way to query the outcome after a timeout; and an explicit statement of atomicity, with the recognition that interdependent items should be atomic and rejected whole.
Quick check
Quiz: Why is HTTP 200 with a success body the most damaging way to report two failures in a batch of 500? Because the client's success path treats the batch as complete, so the failures are never retried and surface - if at all - in a reconciliation weeks later.
Flashcard: Which two properties make a partial-success response safely retryable? A client-supplied identifier on every item so failures can be matched back, and per-item idempotency so resubmitting the failures cannot duplicate the successes.