Designing Clean Bulk and Batch Operations in REST APIs
AI generated
{ }
GET
Bulk Operations · API Design
Designing Clean Bulk and Batch Operations
How a bulk endpoint handles partial failures without confusing clients

When a client needs to create or update hundreds of entities, hundreds of individual HTTP requests are inefficient, both in network overhead and server load. A well-designed bulk endpoint drastically reduces this overhead, but brings its own design challenge: how do you communicate when some items in a batch succeed and others fail?

15 min read Bulk Operations · Batch APIs Symfony · Partial Failure

1. Why individual requests don't scale for large data volumes

Every single HTTP request carries a fixed overhead, TCP handshake (unless a connection is reused), TLS handshake, HTTP header parsing, and routing, which occurs a thousand times over for a thousand individual requests, while it only occurs once for a single bulk request. For use cases like an initial data import, synchronization between systems, or a CSV upload with thousands of rows, this difference is the difference between an operation that takes seconds and one that takes minutes or hours.

Beyond the pure network overhead, a bulk endpoint lets the server batch database operations (for example a single bulk INSERT instead of a thousand individual INSERTs), which also drastically reduces actual processing time on the server side, compared to a thousand separate transactions.

2. Response format for mixed successes and failures

The central design question for bulk endpoints is what the response looks like when eight of ten submitted items were processed successfully and two failed for business reasons (such as a validation rule or a duplicate). A single global HTTP status code can't represent this mixed situation, so the response should contain an array with one result per item, each with its own status, its own ID reference back to the original item, and, where applicable, its own error message.

HTTP 207 Multi-Status, originally from WebDAV, is used by some APIs for exactly this case, but isn't universally established. A pragmatic alternative approach is HTTP 200 with a structured body that explicitly distinguishes between successful and failed items, as long as this convention is clearly documented.


<?php
declare(strict_types=1);

final class BulkOrderCreateController
{
    public function bulkCreate(array $orderPayloads): array
    {
        $results = [];

        foreach ($orderPayloads as $index => $payload) {
            try {
                $order = $this->orderService->create($payload);
                $results[] = [
                    'index' => $index,
                    'status' => 'success',
                    'id' => $order->getId(),
                ];
            } catch (ValidationException $e) {
                $results[] = [
                    'index' => $index,
                    'status' => 'error',
                    'error' => $e->getMessage(),
                ];
            }
        }

        return [
            'total' => count($orderPayloads),
            'succeeded' => count(array_filter($results, fn ($r) => $r['status'] === 'success')),
            'failed' => count(array_filter($results, fn ($r) => $r['status'] === 'error')),
            'results' => $results,
        ];
    }
}

3. Atomic transaction vs. best-effort processing

A bulk endpoint must explicitly decide whether a batch is treated as an atomic unit (either all items succeed or a complete rollback) or operates in best-effort mode (each item is processed independently, individual failures don't block the remaining items). Both approaches have legitimate use cases but very different behavior, which is why the choice should be explicitly communicated and ideally controllable via a query or body parameter.

Atomic semantics fit well with financially sensitive batches, where a partial success would create unacceptable inconsistencies (such as a series of related bookings). Best-effort semantics fit better with independent items, where a single failure shouldn't prevent processing of the remaining, correct items, such as a product catalog import with some faulty records.

4. Setting sensible size limits for batches

A bulk endpoint without a size limit invites a client to try submitting hundreds of thousands of items in a single request, which makes both server-side memory usage and request duration uncontrollable, and in the worst case leads to a timeout without the client knowing whether anything was even processed. An explicit limit (say, a maximum of 500 items per request) with a clear error message when exceeded is therefore mandatory, not optional.

For data volumes larger than the limit allows, the API documentation should explicitly point to paginating the upload (multiple sequential bulk requests) or to an asynchronous bulk import mechanism with job status polling, instead of leaving clients alone with just the error message.

5. Idempotency for repeated bulk requests

On a network error in the middle of a bulk request, the client often doesn't know for certain whether the server already (partially) processed the batch before the response was lost. Without idempotency protection, a naive retry of the entire batch leads to duplicate records for the items that were already processed successfully.

An idempotency key per batch request, analogous to the single-request pattern, prevents this problem: the server recognizes a repeated request with the same key and returns the cached original response again, instead of processing the batch a second time. Additionally, each item in the batch can carry its own client-generated ID to enable server-side duplicate detection at the item level.

6. Asynchronous processing for very large batches

For batches that would exceed the synchronous processing time of a single HTTP request (typically more than a few seconds), the bulk endpoint should instead return HTTP 202 Accepted with a job ID and perform the actual processing asynchronously via a message queue, while the client polls progress through a separate status endpoint. This pattern connects bulk operations with the long-running operations pattern.

The status endpoint should not only return overall progress (such as 340 of 1000 processed) but ideally also already-completed partial results, so a client can react early to individual failures instead of having to wait for the entire processing to finish.

7. Handling ordering and dependencies between batch items

Some batches contain items with dependencies on each other, for example when an item in the same batch references the client-generated ID of another item (an order line referencing a product variant newly created in the same batch). In this case, the server must either guarantee a processing order that respects such dependencies, or clearly document that items are processed independently and in arbitrary order, so clients must resolve dependencies themselves across multiple sequential batches.

Explicit documentation of this guarantee (or its absence) matters, because otherwise clients might implicitly assume an ordering that the server doesn't actually guarantee, which can lead to hard-to-reproduce, race-like failures.

8. Monitoring bulk endpoints separately from single-item endpoints

Bulk endpoints have a fundamentally different load profile than single-item endpoints: a single bulk request can trigger hundreds of database operations internally, which is why standard metrics like requests per second alone are misleading if they lump bulk and single-item endpoints together without separation. More meaningful metrics are items processed per second and average batch size, tracked separately for bulk endpoints.

Equally important is a failure rate tracked per item rather than only per request, since a single failed bulk request with 500 items could still have processed 495 of them successfully, which would show up as a complete failure in a pure request failure rate, even though the actual success rate is 99 percent.

9. Bulk operations at a glance

The table below compares the key design decisions for bulk endpoints.

Aspect Option A Option B
Failure handling Atomic: all or nothing Best-effort: independent items
Processing Synchronous with direct response Asynchronous with job ID and status polling
Response format HTTP 207 Multi-Status HTTP 200 with structured result array
Size limit Hard-limited (e.g., 500 items) Unlimited with asynchronous processing

Mironsoft

OpenAPI design, Symfony APIs, and API security

APIs that external teams can integrate without back-and-forth questions?

We review existing REST APIs for inconsistent error formats, missing OpenAPI documentation, and security gaps, then build an API that is clearly documented, versioned, and hardened against abuse.

API Review

Checking the OpenAPI spec, error formats, and status codes for consistency.

Symfony Implementation

Using DTOs, Serializer, and Validator for clean, type-safe request/response models.

Security Audit

Hardening rate limiting, auth schemes, and input validation against real attack surfaces.

10. Summary

Bulk Operations: The Essentials at a Glance

Why bulk

Reduces network overhead and enables batched database operations compared to thousands of individual requests.

Partial failure

The response needs one result per item, since a single global status code can't represent mixed successes.

Atomic vs. best-effort

Must be explicitly chosen and communicated, both modes have legitimate, different use cases.

Idempotency

An idempotency key per batch prevents duplicate processing on network failures and retries.

11. FAQ: Bulk Operations: The Essentials at a Glance

1At what batch size does a bulk endpoint pay off?
Starting around 10 to 20 items the overhead benefit becomes noticeable. For just 2 to 3 items, the extra design effort usually isn't justified.
2Should I use HTTP 207 Multi-Status?
If your framework and client libraries support it well, yes. Otherwise, HTTP 200 with a clearly documented result array is equally valid.
3How do I prevent a client from sending oversized batches?
With a hard, documented size limit and a clear 400 error message when the limit is exceeded.
4What happens on a timeout in the middle of bulk processing?
Without an idempotency key, the state is unclear. With an idempotency key, a retry can safely re-request the same, already-processed batch.
5Can I mix bulk updates and bulk creates in the same endpoint?
Technically possible via an operation field per item, but it increases complexity. Separate endpoints are usually clearer.
6How do I meaningfully test partial failure handling?
With test cases that deliberately send a batch mixing valid and invalid items and check the result array item by item.
7Is best-effort always the better choice?
No, for financially interdependent operations a partial success can create unacceptable data inconsistencies, where atomic processing is preferable.
8How do I handle dependencies between batch items?
Either document a guaranteed processing order or explicitly rule out dependencies and point clients to sequential batches.
9Does every bulk endpoint need asynchronous processing?
No, only when expected processing time exceeds a few seconds. Smaller batches can stay synchronous.
10How do I monitor bulk endpoint performance?
With metrics on average batch size, processing time per item, and failure rate, tracked separately from single-item endpoint metrics.