Designing Bulk Operations in REST APIs
How to design REST endpoints that accept many items at once: request shape, partial success, error reporting, idempotency, and limits that keep your API healthy under load.
What you'll learn
- ✓When bulk endpoints actually help
- ✓How to shape requests and responses
- ✓Partial success and error reporting patterns
- ✓Idempotency for batches
- ✓Rate limits and chunk sizing
Prerequisites
- •Comfort with REST and HTTP status codes
Bulk endpoints look simple on a whiteboard and get messy in production. The HTTP layer was built around one request, one resource. Once you let a single request touch a hundred rows, every error case multiplies. This post is the playbook I wish I had the first time I shipped one.
What and Why
A bulk operation is a single HTTP request that creates, updates, or deletes more than one resource. The point is not raw speed; the point is round-trip cost. If a client needs to import ten thousand contacts and each create is a single POST, that is ten thousand TLS handshakes, ten thousand auth checks, ten thousand log entries. Batching collapses that overhead and lets the server amortize work like database transactions and index updates.
Bulk endpoints make sense when clients reliably have many items at once: data imports, mobile sync, background jobs, ETL pipelines. They are wrong when items arrive one at a time from user actions; there, single-resource endpoints stay simpler.
Mental Model
Think of a bulk request as a small transcript. The client sends an ordered list of operations. The server processes them, then returns a parallel list of results. Each result is independent and addressable by index or by a client-supplied reference id.
There are two camps. All-or-nothing wraps the whole batch in a transaction; one bad row rolls everything back. Partial success processes each item independently and reports per-item outcomes. Partial success is almost always the right default for public APIs because clients can fix the broken rows without redoing the rest.
Hands-on Example
Here is a bulk create for a contacts resource. Each item carries a ref the client owns, so the response can be matched back without relying on array order.
POST /v1/contacts/bulk
Content-Type: application/json
Idempotency-Key: 2f8c-import-batch-19
{
"items": [
{ "ref": "row-1", "email": "ada@example.com", "name": "Ada" },
{ "ref": "row-2", "email": "not-an-email", "name": "Bad" },
{ "ref": "row-3", "email": "lin@example.com", "name": "Lin" }
]
}
The response uses 207 Multi-Status semantics, but in a single JSON body so clients do not have to parse multipart:
{
"results": [
{ "ref": "row-1", "status": 201, "id": "c_01H..." },
{ "ref": "row-2", "status": 422, "error": { "code": "invalid_email" } },
{ "ref": "row-3", "status": 201, "id": "c_02H..." }
],
"summary": { "succeeded": 2, "failed": 1 }
}
client
| items[ref-1, ref-2, ref-3]
v
[ API gateway ] -- idempotency cache --> reuse prior result?
|
v
[ bulk handler ]
|-- validate item 1 -> insert -> 201
|-- validate item 2 -> reject -> 422
|-- validate item 3 -> insert -> 201
v
results[ref-1:201, ref-2:422, ref-3:201] The outer HTTP status is 200 because the request itself succeeded. Per-item statuses live inside the body. Reserve 4xx on the envelope for whole-batch failures: malformed JSON, missing auth, exceeded size.
Common Pitfalls
- Returning the first error and stopping. Clients then have to retry the whole batch, which often re-runs successful work. Always report every row.
- Relying on array index for correlation. If the server reorders for parallelism, indexes drift. Require a client
ref. - No size cap. Without a max items and max bytes limit, one tenant can pin a worker for minutes. Document both, return
413when exceeded. - Hidden transactions. If you wrap inserts in a single transaction silently, partial success becomes a lie. Be explicit in the docs.
- Bulk delete by query.
DELETE /things?filter=...is tempting and dangerous. Require an explicit id list or a confirmation token.
Practical Tips
Set a hard ceiling, usually 100 to 1000 items, and tell clients to chunk above that. Accept an Idempotency-Key on the whole batch so retries after a network blip do not double-insert. Stream large responses with chunked transfer if results can be megabytes. Emit one log line per batch plus structured per-item errors, not one log per row. For async jobs over a few seconds, return 202 Accepted with a status URL instead of holding the connection open.
Wrap-up
A good bulk endpoint feels boring: predictable shape, partial success, stable correlation, clear limits. Get those four right and you can scale the workload behind it without redesigning the contract. Treat the batch as a transcript, give every row a voice in the response, and your clients will thank you.
Related articles
- REST APIs REST API Design Best Practices: A Practical Guide
Apply REST design best practices for resources, naming, status codes, pagination, and versioning to build clean, durable APIs.
- REST APIs REST API Error Handling Conventions
Design clear, consistent error responses for REST APIs using HTTP status codes, problem details, and error envelopes that clients can actually handle.
- REST APIs Designing Idempotency Keys for REST APIs
A practical guide to idempotency keys: what they are, how to store them, how to handle replays and conflicts, and how to make POST endpoints safe to retry without duplicates.
- REST APIs HTTP Methods Explained: GET, POST, PUT, PATCH, DELETE
A practical walkthrough of HTTP methods — semantics, safety, idempotency, body conventions, and the differences between POST, PUT, and PATCH. Plus OPTIONS and HEAD.