Calling a REST API once per item is perfectly acceptable for a small number of entities (N). However, as N grows large, or as network latency increases, the overhead of individual HTTP requests will catastrophicly degrade system performance.
The batch API design problem is deceptively complex. It is not simply a matter of allowing an array in a JSON payload. Architects must solve for partial failures, atomic transactional boundaries, HTTP semantic mapping, and idempotency guarantees. When a client sends a request to process $100K worth of transactions in a single batch, and the connection drops halfway through, the API design determines whether the system gracefully recovers or double-charges the users.
This guide explores the foundational "why" of batching, the structural patterns that actually work in production, and the dangerous anti-patterns that plague junior implementations.
To understand why batching is mandatory at scale, one must look at the physical overhead of an HTTP request.
If a client needs to create 1,000 users, making 1,000 separate POST /users requests forces the client and server to pay this "tax" 1,000 times. If the network round-trip time is 50ms, the theoretical floor for completing this task serially is 50 seconds, not including server processing time.
The Batching Solution: By combining all 1,000 users into a single POST /users/batch request, the TCP/TLS overhead is paid exactly once. Furthermore, the server can open a single database transaction, utilize a prepared SQL statement (INSERT INTO users ...), and execute a bulk insert. A task that took 50+ seconds serially can easily be completed in 500 milliseconds.
The most common structural pattern for a batch API mirrors the request payload in the response payload.
A well-designed batch request accepts an array of operations. It is critical to enforce a maximum batch size. Without hard limits (e.g., maximum 1,000 items or 5MB payload size), a malicious or buggy client can send an infinitely large JSON array, causing the server to run out of memory (OOM) and crash while attempting to parse the payload.
POST /api/orders/batch
{
"operations": [
{ "id": "req-1", "amount": 100.00 },
{ "id": "req-2", "amount": 250.50 }
]
}
How do you return an HTTP status code if item 1 succeeds but item 2 fails?
200 OK. The 200 signifies only that the batch request was successfully received, parsed, and processed by the server. The actual success or failure of the individual items is buried within the JSON response body.207 Multi-Status code exactly for this scenario, it is notoriously poorly supported by standard HTTP client libraries (like Axios or Fetch) and is generally avoided in modern REST APIs.The defining characteristic of a professional batch API is how it handles partial failures.
In an "All-or-Nothing" design, the server wraps the entire batch in a single database transaction. If 999 items succeed and the 1,000th item fails validation, the server rolls back the transaction. Nothing is saved.
In most modern APIs, items are processed independently. The response explicitly maps the outcome of each requested item.
{
"results": [
{ "id": "req-1", "status": "ok", "created_id": "ord_999" },
{ "id": "req-2", "status": "error", "error_code": "INSUFFICIENT_FUNDS" }
]
}
When dealing with financial or critical data, idempotency is non-negotiable. If a client sends a batch of 1,000 payments, and the server processes them successfully but the Wi-Fi connection drops before the client receives the 200 OK, the client is in a blind state. Did it work?
If the client blindly retries the exact same request, it might charge the users twice.
To solve this, the API must mandate that the client provides a unique "Idempotency Key" for every single item in the batch (often a UUID generated by the client).
Different use cases demand entirely different architectural implementations.
For small, fast operations (e.g., inserting 500 telemetry logs into a database), the server processes the batch synchronously. The client holds the HTTP connection open, and the server returns the results within a few seconds.
If a batch contains 50,000 items, or if processing each item requires slow third-party API calls, the synchronous model breaks. The HTTP connection will time out (usually restricted to 30 or 60 seconds by API Gateways).
202 Accepted and a job_id.GET /api/batch/jobs/{job_id}) every few seconds to check the status. Once the job is marked COMPLETED, the client downloads the results array.Polling is inherently inefficient. For enterprise-grade batch APIs (like bulk exporting data from Salesforce), the client provides a webhook_url in the initial request. When the server finally finishes processing the massive batch three hours later, it actively sends an HTTP POST request to the client's webhook URL containing the final results.
/batch endpoint, but the backend simply runs a for loop over the array, making 1,000 separate database queries. This saves network TCP overhead but completely fails to optimize the database layer. A true batch API utilizes bulk database operations (e.g., INSERT INTO ... VALUES (), (), ()).Batch API design forces architects to confront the chaotic reality of distributed systems. Networks fail, databases lock, and clients send malformed data. By embracing item-level idempotency, clear partial-failure contracts, and explicit asynchronous patterns for heavy workloads, engineering teams can build resilient bulk-transfer systems that scale gracefully.