Designing API Bulk Update Endpoints: The Partial Update Pattern Customers Actually Use

How to extend the single-item update semantic to operate on hundreds of resources at once without producing the half-applied failure mode that bulk endpoints commonly inherit from naive scaling of single-item APIs.

The single-item PATCH endpoint is well-understood: customer sends the fields they want to change, server applies them atomically, returns the new state. The bulk version of this operation is conceptually simple but adds failure modes that the single-item endpoint does not face, and the design choices that handle those failure modes well determine whether the endpoint is genuinely useful or whether customers end up writing client-side loops anyway.

The partial-success problem

The single-item PATCH either succeeds or fails; there is no middle ground. The bulk PATCH operating on N items can have any combination of N successes and N failures, and the API has to communicate which is which in a way that the customer can act on.

Three patterns that handle this badly are worth naming. The all-or-nothing pattern rolls back any partial results if any item fails, which feels clean but is rarely what customers want for bulk operations: a customer doing a 500-item update almost always prefers 499 successes and one explicit failure over 500 rollbacks because one item had a stale version. The 200-on-any-success pattern returns success if at least one item succeeded, which makes the response code useless for customer error handling. The single-error-on-any-failure pattern returns a 4xx for the whole batch on first failure, requiring customers to retry the operation and figure out which item to skip.

The right default is 200 OK with per-item statuses in the response body. Each item gets its own result with status, optional error code, and optional updated resource. The customer can iterate the response and retry only the failed items, and the response code itself indicates the request reached the server and was processed.

The minimum viable shape

The request shape is straightforward: an array of update operations, each with the resource identifier and the fields to change. The response shape mirrors this: an array of per-item results in the same order as the request, plus a summary block with counts of succeeded and failed items.

POST /api/v1/bulk-update
{
  "items": [
    {"id": "abc123", "fields": {"status": "active"}},
    {"id": "def456", "fields": {"status": "active"}},
    ...
  ]
}

200 OK
{
  "summary": {"total": 2, "succeeded": 1, "failed": 1},
  "results": [
    {"id": "abc123", "status": "succeeded", "resource": {...}},
    {"id": "def456", "status": "failed", "error": {"code": "not_found", "message": "..."}}
  ]
}

The same-order constraint matters because it lets customers correlate results with their original input without having to look up by ID. A customer who built the input from a CSV file in a specific order can iterate the results in the same order to know which CSV rows succeeded.

Idempotency

Idempotency for bulk endpoints has two flavors: per-batch and per-item. Per-batch is the simpler approach: the customer sends an idempotency key with the whole batch, and a retry with the same key returns the cached response. Per-item is more flexible but more complex: each item has its own idempotency key, and individual items can be retried without retrying the whole batch.

Per-batch is the right default for most use cases. It handles the common retry case (network timeout produces uncertainty about whether the batch was applied) and is easy to implement with a simple unique-key store. The 24-72 hour TTL on the cache is enough for the typical retry window.

Per-item idempotency is worth implementing for endpoints where the customer is likely to build batches dynamically (combining results from multiple sources) and may want to retry individual items without retrying the whole batch. It costs more storage and more implementation complexity, so the right time to add it is in response to documented customer demand.

Validation

The validation question is whether to validate the entire batch before applying any of it, or to validate per-item and apply what passes. Per-item is the right default for bulk endpoints because it preserves the partial-success semantics: a customer should be able to send a batch with one bad item and have the other items succeed.

The pre-flight-validation alternative is a separate endpoint that the customer can call to check whether a batch would succeed without actually applying it. This is useful for high-stakes operations where the customer wants to surface validation errors to a human before committing. The shape is identical to the regular bulk endpoint but with a dry-run flag in the URL or body.

Rate limiting and quotas

Bulk endpoints have to count against item-based quotas, not request-based rate limits. A customer paying for 10,000 updates per month should not be able to bypass the quota by batching them into 100-item requests; the bulk endpoint has to credit 100 items against the quota for each batch.

The rate-limit accounting is more subtle. A 500-item bulk update is more expensive than a single update, but it is much cheaper per-item than 500 separate requests. The right pattern is to count items for rate limiting too, but with a discount for batching: maybe 0.5 items per item, or a flat overhead plus per-item cost. The exact formula depends on your backend cost structure.

Synchronous vs async

The cutover from synchronous to asynchronous processing happens around 100-200 items for typical operations. Below that, the customer wants the response in the same request and is willing to wait a few hundred milliseconds. Above that, the request times out or holds connections open long enough to cause problems.

The async pattern returns 202 Accepted with a job ID, and the customer polls or receives a webhook when the job completes. The status endpoint returns the same per-item results structure that the synchronous endpoint would have returned. This pattern is well-trodden and customer expectations are clear for it.

The middle case (100-1000 items, sometimes fast and sometimes slow) is the awkward one. The right answer is usually to make the sync endpoint return promptly with a degraded result (process what you can in 30 seconds and return) and offer the async endpoint for cases where the customer needs to know the entire batch is done.

Three patterns that fail

The first is treating bulk endpoints as a thin wrapper around the single-item endpoint with no thought to failure modes. The result is endpoints that work great for the happy path and produce confusing behavior on any failure.

The second is requiring the customer to specify a unique idempotency key per item even when they did not need per-item idempotency. This pushes implementation complexity onto every customer for the benefit of a few use cases.

The third is silently truncating batches that exceed an undocumented size limit. If the endpoint has a maximum batch size, the size has to be documented and over-size requests have to be rejected with a clear error, not silently truncated.

Our use across products

Our four products (DocuMint, CronPing, FlagBit, WebhookVault) implement bulk update on the resources where customer demand for it has been documented: FlagBit for bulk flag updates during a feature retirement campaign, CronPing for bulk monitor pause and resume, WebhookVault for bulk endpoint configuration changes. DocuMint has not needed a bulk update endpoint because the invoice generation is fundamentally per-document. The shared library across products implements the same response shape and the per-batch idempotency primitive, with per-item idempotency planned only when we see customers asking for it.

Deeper observation

Bulk endpoints are one of those places where the same underlying operation has a qualitatively different API design when applied at scale rather than per-item. The single-item update is a simple request-response operation; the bulk update is a batch processing operation with summary statistics, partial-success semantics, and idempotency concerns that the single-item version does not face. Treating the bulk endpoint as a separate primitive with its own design decisions tends to produce APIs that customers can actually build reliable integrations against.

Read more