Batch Operations in APIs: Designing for Bulk Without Breaking Everything
The standard REST pattern is one resource per request. Create one user, update one order, fetch one product. This works until an integrator needs to create ten thousand users, update five hundred orders, or sync an entire product catalog. Making ten thousand individual API calls is slow, expensive in terms of rate limit quota, and brittle — a network failure on request 7,432 leaves the integrator with a partially completed operation and no clean recovery path.
Batch APIs solve the N-request problem by allowing multiple operations in a single HTTP call. Designing them well requires making several non-obvious choices about scope, atomicity, error handling, and size limits.
Collection-Level Batch Operations
The simplest batch pattern applies a single operation to multiple resources simultaneously. Create multiple records in one request, delete multiple records in one request, update a specific field across a set of matching records.
Batch create:
POST /users/batch HTTP/1.1
Content-Type: application/json
{
"users": [
{"email": "[email protected]", "name": "Alice"},
{"email": "[email protected]", "name": "Bob"},
{"email": "[email protected]", "name": "Carol"}
]
}
Batch delete using a request body or query parameter listing IDs:
DELETE /products/batch HTTP/1.1
Content-Type: application/json
{"ids": ["prod_abc", "prod_def", "prod_ghi"]}
Bulk update applying a change to a filtered set:
PATCH /orders/batch HTTP/1.1
Content-Type: application/json
{
"filter": {"status": "pending", "created_before": "2026-01-01"},
"update": {"status": "archived"}
}
These operations are efficient when all the items in the batch are subject to the same operation. They become insufficient when each item in the batch needs different handling.
Mixed-Operation Batching
Some batch APIs allow a single request to contain multiple distinct operations — a mix of creates, updates, and deletes across different resources. Stripe’s batch endpoint and Google’s batch request format follow this model:
POST /batch HTTP/1.1
Content-Type: application/json
{
"requests": [
{"method": "POST", "path": "/users", "body": {"email": "[email protected]"}},
{"method": "PATCH", "path": "/users/usr_123", "body": {"name": "Robert"}},
{"method": "DELETE", "path": "/users/usr_456"}
]
}
The response contains results for each operation in the same order:
{
"responses": [
{"status": 201, "body": {"id": "usr_789", "email": "[email protected]"}},
{"status": 200, "body": {"id": "usr_123", "name": "Robert"}},
{"status": 204, "body": null}
]
}
Each operation executes independently. One failing does not block the others. This model is flexible — any combination of operations can be submitted — but the implementation is more complex and the response parsing requires more client logic.
Atomicity: All-or-Nothing vs Best-Effort
The most consequential design decision in batch API design is atomicity. Does the entire batch succeed or fail together, or does each item in the batch succeed or fail independently?
Atomic batches wrap all operations in a transaction. If item three fails, items one and two are rolled back. The entire batch either completes cleanly or has no effect. This is the correct choice when the operations are logically interdependent — creating an order with its line items, where partial success would leave corrupted data. The cost is that one bad record in a batch of ten thousand invalidates the entire operation.
Best-effort batches process each item independently. Items that succeed are committed; items that fail are reported in the response. The result is a partial success state — some items processed, some not — that the client must handle. This is appropriate when items are logically independent and partial processing is acceptable. An integrator uploading a product catalog where some items have invalid fields wants the valid items to succeed while seeing a clear list of what failed and why.
Be explicit about which model your batch endpoint uses. Integrators cannot assume either behavior and will write broken error handling if the documentation is ambiguous.
Error Reporting in Batch Responses
Best-effort batch APIs need a clear error reporting format. Each item needs to carry its own status — success or failure — along with error details for failed items:
{
"summary": {
"total": 5,
"succeeded": 4,
"failed": 1
},
"results": [
{"index": 0, "status": "success", "id": "usr_aaa"},
{"index": 1, "status": "success", "id": "usr_bbb"},
{"index": 2, "status": "error", "error": {"code": "duplicate_email", "message": "Email already exists."}},
{"index": 3, "status": "success", "id": "usr_ccc"},
{"index": 4, "status": "success", "id": "usr_ddd"}
]
}
Include the original index (or a client-provided identifier) so the integrator can correlate results with the input items. A batch of five thousand items where the response identifies failures only by position requires the integrator to maintain the original ordering to map results back to inputs. If the client submits items with their own identifiers, echo those identifiers in the response.
The overall HTTP status code for a partial success is debatable. 200 with error items in the response body is common and practical, though it means clients must parse the body to know whether everything succeeded. 207 Multi-Status is the technically correct HTTP status for responses where different items have different statuses, though it is less commonly used in REST APIs. Pick one approach and document it clearly.
Size Limits and Async Execution
Every batch endpoint needs size limits. Without them, a client submitting a batch of one million items turns a batch request into an unacceptable resource consumption event. Define and document maximum batch sizes (number of items, maximum request body size in bytes) and return a clear error when the limit is exceeded.
For batch operations that will take longer than a few seconds, return a 202 Accepted and process asynchronously rather than blocking the HTTP connection. The client receives a job ID and checks status — the same pattern as async job APIs. Synchronous batch processing has an implicit upper bound on how many items can be processed within a request timeout; async batch processing removes that constraint entirely.
Idempotency in Batches
Batches that create resources need the same idempotency guarantees as individual create operations. An idempotency key on the batch request protects against a batch being submitted twice — due to a network timeout or a client retry — and creating duplicate records. Implement idempotency keys at the batch level: the same batch key submitted twice produces the same result, not duplicate processing.
For item-level idempotency within a batch (where the client wants each item to be deduplicated independently), allow client-provided identifiers on each item that the server uses to detect and skip duplicates.
Batch APIs are infrastructure for integrations at volume. They are worth the additional design investment when the use case calls for them, and they should not be added prematurely when individual API calls are sufficient. The right time to build a batch endpoint is when you have evidence that integrators need to operate at a scale where individual calls are impractical — not as a speculative feature.