- Published on
A bulk response must say which items succeeded
- Authors

- Name
- Mehdi Akiki
Article · Interrupted execution
The server returns “87 succeeded, 13 failed.” The client submitted a hundred records.
Which thirteen should it retry?
Counts are useful for a progress screen. They are not a recovery protocol. A bulk endpoint needs a result that can be connected to each submitted item, even when processing finishes out of order.
Define the unit of success
First decide whether the batch commits together or each item can succeed independently. Both contracts are valid. Mixing them in one vague response creates trouble.
For independent outcomes, a Rust model might use a caller-supplied stable item key:
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
enum ItemOutcome {
Committed { remote_id: String },
Rejected { reason_code: String },
Unresolved,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct ItemResult {
item_key: String,
outcome: ItemOutcome,
}
The key identifies the logical item across retries. It is not necessarily the item's position in one array. If the same key appears twice in a submission, reject the ambiguity or apply a clearly documented duplicate rule.
The partial-failure article explains the wider recovery problem. Here the response format is what gives the client enough information to act on that design.
Do not make ordering carry identity
Parallel processing can reorder results. A provider may omit one result or return a batch-level error after accepting some work.
Match by stable identity and require exactly one result for every submitted item key. A missing result is unresolved until the API gives you stronger evidence. It is not automatically a rejected item ready to repeat.
For an atomic batch, state that all items share one commit outcome. If the commit result is uncertain, the whole operation may need lookup by batch identity. Per-item success messages produced before commit do not establish independent committed writes.
Give retries a smaller target
Retry only items whose outcome and duplicate contract permit it. Preserve their logical identities and unchanged payload meaning. Do not resubmit successful items with fresh keys because the request builder makes that easy.
The batch-size guide covers the cost of large recovery units. A smaller batch can reduce rework, but a precise response is still necessary.
If the entire HTTP response is lost, the client needs an operation lookup or a documented same-key replay. The structured response helps only when it arrives.
Google's batch-update guidance is an example of a published API design contract. Your endpoint should state its own atomicity, error, and long-running-operation semantics just as explicitly.
Exercise mixed and missing outcomes
Use a fixture with success, validation rejection, temporary failure, and an uncertain provider result. Return results in a different order from the input. Omit one result deliberately and confirm that the client does not pair later results with the wrong item.
Then lose the full response and exercise recovery. Count committed effects in storage after replay.
The result can still include totals for the interface. Keep them derived from the actual item outcomes. A reassuring percentage is helpful only when the client can also tell which work is safe to retry.
Recover the missing item before retrying the batch
The runnable fixture submits independently keyed items to a PostgreSQL-backed store. One item commits, one is rejected for invalid quantity, another commits but is omitted from the response, and a fourth is known not to have been accepted. The response is deliberately reordered.
Matching uses item keys, not vector positions. The omitted result becomes Unresolved. Duplicate response keys, unknown keys, and duplicate input identities are rejected as ambiguous. A single top-level success flag cannot express these outcomes accurately enough for recovery.
The fixture looks up the omitted item and finds its committed effect. It does not retry that item with a new identity. The known-unaccepted item is the smaller retry target. After recovery, the stored keys are a, c, and d. Replaying an already committed item with the same payload leaves its existing effect; changing the payload under that key produces a conflict result.
The Rust implementation keeps the matching function beside the store and state assertions. Its response omission is a controlled missing result in memory. It does not claim to simulate a broken TCP connection. The separate uncertain-write HTTP reproduction exercises an actual connection loss after commit.
This distinction also matters for a real bulk provider. Some endpoints are atomic, some commit each item, and some accept a job whose final item results arrive later. Choose the result model from the documented contract. Do not retrofit per-item certainty onto an API that supplies only an unresolved job reference.
For a user-facing import, preserve the original item identity and reason code so the person can correct rejected input without resubmitting known successes. Keep unresolved items visible until lookup or reconciliation supplies evidence. A smaller, justified retry is easier to explain than a second entire batch whose duplicate effects nobody can distinguish from new work.