Bulk Operations
BULK ENDPOINTS AMORTIZE PER-REQUEST OVERHEAD ACROSS MANY OPERATIONS IN A SINGLE HTTP CALL — BUT THE REAL DESIGN CHALLENGE IS WHAT TO RETURN WHEN SOME OPERATIONS SUCCEED AND OTHERS FAIL: TRANSACTIONAL SEMANTICS ROLL BACK EVERYTHING ON ANY ERROR WHILE BEST-EFFORT SEMANTICS COMMIT SUCCESSES AND REPORT FAILURES INDIVIDUALLY, AND CHOOSING THE WRONG MODEL PRODUCES SILENT DATA CORRUPTION OR WASTED RETRIES.
When to Use
- Designing an import endpoint that creates or updates hundreds of records from a CSV upload
- Implementing a bulk delete for a UI that allows multi-select and mass removal
- Building a notification service that sends messages to thousands of recipients in a single API call
- Reducing the number of round trips for a client that must create 50 resources sequentially
- Reviewing a PR that loops over individual create/update/delete endpoints in a batch job
- Choosing between all-or-nothing transaction semantics and best-effort per-item semantics for a batch endpoint
- Adding
Idempotency-Key support to a bulk create endpoint that may be retried on network failure
- Designing partial failure responses and deciding which HTTP status code to return when some items fail
Instructions
Key Concepts
Bulk Create, Update, and Delete — A bulk endpoint accepts an array of operation items in the request body and executes them as a collection. The three primary patterns are:
- Bulk create:
POST /resources/bulk with a body {"items": [...]} creates multiple resources in one request.
- Bulk update:
PATCH /resources/bulk applies a set of patch operations to multiple existing resources.
- Bulk delete:
DELETE /resources/bulk with a body listing resource IDs, or POST /resources/bulk-delete for clients that cannot send DELETE bodies.
Transactional vs. Best-Effort Semantics — This is the most consequential design decision for a bulk endpoint:
- Transactional: All items are applied in a single database transaction. If any item fails validation or encounters an error, the entire batch is rolled back. The response is either
200 OK (all succeeded) or 422 Unprocessable Entity / 400 Bad Request (none were applied, with per-item errors). Use this when partial application would leave data in an inconsistent state.
- Best-effort: Each item is applied independently. Successes are committed; failures are reported per-item. The response uses
207 Multi-Status with a per-item result array. Use this when partial success is acceptable (e.g., sending notifications — some succeed, others fail) and callers can retry only the failed items.
207 Multi-Status Response — When using best-effort semantics, return 207 Multi-Status with a body that maps each input item to an individual status code and optional error detail:
{
"results": [
{ "index": 0, "status": 201, "id": "res_001" },
{
"index": 1,
"status": 422,
"error": { "code": "validation_failed", "message": "name is required" }
},
{ "index": 2, "status": 201, "id": "res_003" }
]
}
The outer HTTP status is 207 regardless of how many items failed. Never return 200 OK for a best-effort batch where some items failed — it misleads callers into thinking all items succeeded.
Idempotency-Key on Bulk Endpoints — A bulk create endpoint that fails mid-way through leaves the caller uncertain about which items were created. Re-submitting the full batch without an idempotency key will duplicate items. Accept an Idempotency-Key header on bulk create and bulk update endpoints. Store the key with a short TTL and replay the previous response on a duplicate submission. For best-effort batches, the idempotency key covers the entire batch — if the batch partially succeeded and the client retries with the same key, replay the original (partial success) response rather than re-executing.
Request Size Limits — Impose an explicit maximum on the number of items per batch and the total request body size in bytes. Document both limits. Common limits: 100–1,000 items per batch, 5–10 MB body size. Requests exceeding these limits should return 413 Content Too Large (body size) or 400 Bad Request (item count) with the limits stated in the error body. Do not silently truncate the input.
Ordering and Dependency — Document whether the items in a bulk request are processed in order and whether later items may depend on earlier ones (e.g., bulk create where item 2 references the ID of item 1). Most bulk endpoints treat items as independent and process them in unspecified order. If ordering matters, process sequentially in input order and stop on first failure (transactional) or continue and report each failure (best-effort).
Worked Example
Stripe's Bulk operations pattern via the Batch API demonstrates the design of a transactional batch using per-operation result codes:
Bulk create price and product in a single batch call:
POST /v1/batch
Content-Type: application/json
Authorization: Bearer sk_example_...
Idempotency-Key: batch_20240315_import_001
{
"requests": [
{
"method": "POST",
"url": "/v1/products",
"body": "name=Widget+Pro&description=Our+flagship+product"
},
{
"method": "POST",
"url": "/v1/prices",
"body": "product=prod_abc&unit_amount=2999¤cy=usd"
}
]
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"results": [
{
"status": 200,
"body": {
"id": "prod_abc123",
"object": "product",
"name": "Widget Pro"
}
},
{
"status": 200,
"body": {
"id": "price_xyz789",
"object": "price",
"unit_amount": 2999,
"currency": "usd",
"product": "prod_abc123"
}
}
]
}
Bulk delete with best-effort semantics (207 Multi-Status):
POST /api/v1/messages/bulk-delete
Content-Type: application/json
Authorization: Bearer token_...
{
"ids": ["msg_001", "msg_002", "msg_003"]
}
HTTP/1.1 207 Multi-Status
Content-Type: application/json
{
"results": [
{ "id": "msg_001", "status": 204 },
{ "id": "msg_002", "status": 404, "error": { "code": "not_found", "message": "Message not found" } },
{ "id": "msg_003", "status": 204 }
],
"summary": { "succeeded": 2, "failed": 1 }
}
Anti-Patterns
Returning 200 OK when some items in a best-effort batch failed. A 200 OK response signals complete success to HTTP clients, middleware, and monitoring systems. If item 3 of 10 failed and the response is 200 OK, automated retry logic and dashboards will not flag the failure. Return 207 Multi-Status for any best-effort batch response that contains a mixture of success and failure, or 422/400 for a transactional batch that failed entirely.
Silently truncating batches that exceed the size limit. If a client sends 500 items and the server's limit is 100, silently processing only the first 100 and returning success causes the caller to believe all 500 were processed. The remaining 400 items are silently dropped. Always validate the item count before processing and return 400 Bad Request with the limit in the error body if exceeded.
Applying transactional semantics to operations that cannot be rolled back. If a bulk endpoint sends emails, charges cards, or triggers external webhooks as each item is processed, these side effects cannot be rolled back when a later item fails. Applying transactional semantics implies rollback is possible. Use best-effort semantics for operations with irreversible external side effects, document the semantics clearly, and design callers to handle partial success.
Omitting per-item indexing in the error response. A 207 response that lists errors without correlating them to the input items forces callers to guess which items failed. Always include the input index (0-based or 1-based, consistently) or the input item's client-provided ID in every result entry. This allows callers to retry only the failed items without re-submitting the entire batch.
Details
Choosing Transactional vs. Best-Effort
| Factor |
Transactional |
Best-Effort |
| Data consistency requirement |
Strong — all or nothing |
Eventual — partial success OK |
| External side effects |
None or fully reversible |
Possible; cannot be rolled back |
| Retry behavior |
Client retries full batch |
Client retries failed items only |
| Response status on partial fail |
400/422 — nothing applied |
207 — successes committed |
| Database transaction |
Single transaction |
Per-item transactions |
| Use cases |
Financial ledger, referential imports |
Notifications, bulk tag application |
Idempotency Key Scope for Bulk
When a client retries a bulk request with the same Idempotency-Key:
- Transactional batch that fully succeeded: Replay the original
200 OK response. Do not re-execute.
- Transactional batch that fully failed: Re-execute (the original transaction was rolled back; no state change occurred).
- Best-effort batch: Replay the original
207 response. Do not re-execute any item, including failed items. The client should re-submit only the failed items with a new idempotency key.
Real-World Case Study: Mailchimp Batch API
Mailchimp's /3.0/batches endpoint processes bulk subscriber list updates asynchronously. Synchronous per-subscriber API calls for a 10,000-subscriber import produced approximately 35 minutes of sequential HTTP round trips from clients with standard connection limits. After migrating to the batch API, the same 10,000-subscriber import completed in under 90 seconds — a 23x throughput improvement. The batch API uses best-effort semantics and returns an operation ID immediately; clients poll GET /3.0/batches/{batch_id} for completion and retrieve a per-item error report from a signed S3 URL included in the completed batch response.
Source
Process
- Decide on semantics: transactional (all-or-nothing, single DB transaction, return
400/422 on any failure) or best-effort (per-item commit, return 207 Multi-Status with per-item results). Document the choice in the API reference.
- Define and enforce request limits: maximum item count, maximum body size in bytes. Return
413 Content Too Large or 400 Bad Request with limits in the error body for violations.
- Add
Idempotency-Key header support. Store the key with the full response and TTL. On duplicate submission, replay the stored response without re-executing.
- Implement per-item result reporting: every result entry must include the input index or client-provided ID, the HTTP status for that item, and for failures, a machine-readable error code and human-readable message.
- For best-effort batches with external side effects (email, webhooks, charges), document that failed items are not retried by the server and callers are responsible for resubmitting failures.
Harness Integration
- Type: knowledge -- this skill is a reference document, not a procedural workflow.
- No tools or state -- consumed as context by other skills and agents.
- related_skills: api-idempotency-keys, api-error-contracts, api-http-methods, api-status-codes
Success Criteria
- Bulk endpoints with best-effort semantics return
207 Multi-Status, never 200 OK, when any item fails.
- Per-item results in
207 responses include the input index or client-provided ID for every item, success or failure.
- Request size limits (item count and body bytes) are enforced and documented; violations return
400/413 with the limit values in the error body.
- Bulk create and bulk update endpoints accept and honor
Idempotency-Key headers; duplicate submissions replay the stored response without re-executing.
- API documentation explicitly states whether semantics are transactional or best-effort for every bulk endpoint.
1---2name: api-bulk-operations3description: Bulk Operations4---5# Bulk Operations67> BULK ENDPOINTS AMORTIZE PER-REQUEST OVERHEAD ACROSS MANY OPERATIONS IN A SINGLE HTTP CALL — BUT THE REAL DESIGN CHALLENGE IS WHAT TO RETURN WHEN SOME OPERATIONS SUCCEED AND OTHERS FAIL: TRANSACTIONAL SEMANTICS ROLL BACK EVERYTHING ON ANY ERROR WHILE BEST-EFFORT SEMANTICS COMMIT SUCCESSES AND REPORT FAILURES INDIVIDUALLY, AND CHOOSING THE WRONG MODEL PRODUCES SILENT DATA CORRUPTION OR WASTED RETRIES.89## When to Use1011- Designing an import endpoint that creates or updates hundreds of records from a CSV upload12- Implementing a bulk delete for a UI that allows multi-select and mass removal13- Building a notification service that sends messages to thousands of recipients in a single API call14- Reducing the number of round trips for a client that must create 50 resources sequentially15- Reviewing a PR that loops over individual create/update/delete endpoints in a batch job16- Choosing between all-or-nothing transaction semantics and best-effort per-item semantics for a batch endpoint17- Adding `Idempotency-Key` support to a bulk create endpoint that may be retried on network failure18- Designing partial failure responses and deciding which HTTP status code to return when some items fail1920## Instructions2122### Key Concepts23241. **Bulk Create, Update, and Delete** — A bulk endpoint accepts an array of operation items in the request body and executes them as a collection. The three primary patterns are:25 - **Bulk create:** `POST /resources/bulk` with a body `{"items": [...]}` creates multiple resources in one request.26 - **Bulk update:** `PATCH /resources/bulk` applies a set of patch operations to multiple existing resources.27 - **Bulk delete:** `DELETE /resources/bulk` with a body listing resource IDs, or `POST /resources/bulk-delete` for clients that cannot send DELETE bodies.28292. **Transactional vs. Best-Effort Semantics** — This is the most consequential design decision for a bulk endpoint:30 - **Transactional:** All items are applied in a single database transaction. If any item fails validation or encounters an error, the entire batch is rolled back. The response is either `200 OK` (all succeeded) or `422 Unprocessable Entity` / `400 Bad Request` (none were applied, with per-item errors). Use this when partial application would leave data in an inconsistent state.31 - **Best-effort:** Each item is applied independently. Successes are committed; failures are reported per-item. The response uses `207 Multi-Status` with a per-item result array. Use this when partial success is acceptable (e.g., sending notifications — some succeed, others fail) and callers can retry only the failed items.32333. **207 Multi-Status Response** — When using best-effort semantics, return `207 Multi-Status` with a body that maps each input item to an individual status code and optional error detail:3435 ```json36 {37 "results": [38 { "index": 0, "status": 201, "id": "res_001" },39 {40 "index": 1,41 "status": 422,42 "error": { "code": "validation_failed", "message": "name is required" }43 },44 { "index": 2, "status": 201, "id": "res_003" }45 ]46 }47 ```4849 The outer HTTP status is `207` regardless of how many items failed. Never return `200 OK` for a best-effort batch where some items failed — it misleads callers into thinking all items succeeded.50514. **Idempotency-Key on Bulk Endpoints** — A bulk create endpoint that fails mid-way through leaves the caller uncertain about which items were created. Re-submitting the full batch without an idempotency key will duplicate items. Accept an `Idempotency-Key` header on bulk create and bulk update endpoints. Store the key with a short TTL and replay the previous response on a duplicate submission. For best-effort batches, the idempotency key covers the entire batch — if the batch partially succeeded and the client retries with the same key, replay the original (partial success) response rather than re-executing.52535. **Request Size Limits** — Impose an explicit maximum on the number of items per batch and the total request body size in bytes. Document both limits. Common limits: 100–1,000 items per batch, 5–10 MB body size. Requests exceeding these limits should return `413 Content Too Large` (body size) or `400 Bad Request` (item count) with the limits stated in the error body. Do not silently truncate the input.54556. **Ordering and Dependency** — Document whether the items in a bulk request are processed in order and whether later items may depend on earlier ones (e.g., bulk create where item 2 references the ID of item 1). Most bulk endpoints treat items as independent and process them in unspecified order. If ordering matters, process sequentially in input order and stop on first failure (transactional) or continue and report each failure (best-effort).5657### Worked Example5859Stripe's Bulk operations pattern via the Batch API demonstrates the design of a transactional batch using per-operation result codes:6061**Bulk create price and product in a single batch call:**6263```http64POST /v1/batch65Content-Type: application/json66Authorization: Bearer sk_example_...67Idempotency-Key: batch_20240315_import_0016869{70 "requests": [71 {72 "method": "POST",73 "url": "/v1/products",74 "body": "name=Widget+Pro&description=Our+flagship+product"75 },76 {77 "method": "POST",78 "url": "/v1/prices",79 "body": "product=prod_abc&unit_amount=2999¤cy=usd"80 }81 ]82}83```8485```http86HTTP/1.1 200 OK87Content-Type: application/json8889{90 "results": [91 {92 "status": 200,93 "body": {94 "id": "prod_abc123",95 "object": "product",96 "name": "Widget Pro"97 }98 },99 {100 "status": 200,101 "body": {102 "id": "price_xyz789",103 "object": "price",104 "unit_amount": 2999,105 "currency": "usd",106 "product": "prod_abc123"107 }108 }109 ]110}111```112113**Bulk delete with best-effort semantics (207 Multi-Status):**114115```http116POST /api/v1/messages/bulk-delete117Content-Type: application/json118Authorization: Bearer token_...119120{121 "ids": ["msg_001", "msg_002", "msg_003"]122}123```124125```http126HTTP/1.1 207 Multi-Status127Content-Type: application/json128129{130 "results": [131 { "id": "msg_001", "status": 204 },132 { "id": "msg_002", "status": 404, "error": { "code": "not_found", "message": "Message not found" } },133 { "id": "msg_003", "status": 204 }134 ],135 "summary": { "succeeded": 2, "failed": 1 }136}137```138139### Anti-Patterns1401411. **Returning `200 OK` when some items in a best-effort batch failed.** A `200 OK` response signals complete success to HTTP clients, middleware, and monitoring systems. If item 3 of 10 failed and the response is `200 OK`, automated retry logic and dashboards will not flag the failure. Return `207 Multi-Status` for any best-effort batch response that contains a mixture of success and failure, or `422`/`400` for a transactional batch that failed entirely.1421432. **Silently truncating batches that exceed the size limit.** If a client sends 500 items and the server's limit is 100, silently processing only the first 100 and returning success causes the caller to believe all 500 were processed. The remaining 400 items are silently dropped. Always validate the item count before processing and return `400 Bad Request` with the limit in the error body if exceeded.1441453. **Applying transactional semantics to operations that cannot be rolled back.** If a bulk endpoint sends emails, charges cards, or triggers external webhooks as each item is processed, these side effects cannot be rolled back when a later item fails. Applying transactional semantics implies rollback is possible. Use best-effort semantics for operations with irreversible external side effects, document the semantics clearly, and design callers to handle partial success.1461474. **Omitting per-item indexing in the error response.** A `207` response that lists errors without correlating them to the input items forces callers to guess which items failed. Always include the input index (0-based or 1-based, consistently) or the input item's client-provided ID in every result entry. This allows callers to retry only the failed items without re-submitting the entire batch.148149## Details150151### Choosing Transactional vs. Best-Effort152153| Factor | Transactional | Best-Effort |154| ------------------------------- | ------------------------------------- | ----------------------------------- |155| Data consistency requirement | Strong — all or nothing | Eventual — partial success OK |156| External side effects | None or fully reversible | Possible; cannot be rolled back |157| Retry behavior | Client retries full batch | Client retries failed items only |158| Response status on partial fail | `400`/`422` — nothing applied | `207` — successes committed |159| Database transaction | Single transaction | Per-item transactions |160| Use cases | Financial ledger, referential imports | Notifications, bulk tag application |161162### Idempotency Key Scope for Bulk163164When a client retries a bulk request with the same `Idempotency-Key`:165166- **Transactional batch that fully succeeded:** Replay the original `200 OK` response. Do not re-execute.167- **Transactional batch that fully failed:** Re-execute (the original transaction was rolled back; no state change occurred).168- **Best-effort batch:** Replay the original `207` response. Do not re-execute any item, including failed items. The client should re-submit only the failed items with a new idempotency key.169170### Real-World Case Study: Mailchimp Batch API171172Mailchimp's `/3.0/batches` endpoint processes bulk subscriber list updates asynchronously. Synchronous per-subscriber API calls for a 10,000-subscriber import produced approximately 35 minutes of sequential HTTP round trips from clients with standard connection limits. After migrating to the batch API, the same 10,000-subscriber import completed in under 90 seconds — a 23x throughput improvement. The batch API uses best-effort semantics and returns an operation ID immediately; clients poll `GET /3.0/batches/{batch_id}` for completion and retrieve a per-item error report from a signed S3 URL included in the completed batch response.173174## Source175176- [Stripe Batch API](https://stripe.com/docs/api/bulk)177- [RFC 4918 — HTTP Extensions for WebDAV (207 Multi-Status)](https://www.rfc-editor.org/rfc/rfc4918#section-13)178- [Mailchimp Batch Operations](https://mailchimp.com/developer/marketing/api/batch-operations/)179- [Microsoft Graph Batch Requests](https://learn.microsoft.com/en-us/graph/json-batching)180- [Stripe Idempotency Keys](https://stripe.com/docs/api/idempotent_requests)181182## Process1831841. Decide on semantics: transactional (all-or-nothing, single DB transaction, return `400`/`422` on any failure) or best-effort (per-item commit, return `207 Multi-Status` with per-item results). Document the choice in the API reference.1852. Define and enforce request limits: maximum item count, maximum body size in bytes. Return `413 Content Too Large` or `400 Bad Request` with limits in the error body for violations.1863. Add `Idempotency-Key` header support. Store the key with the full response and TTL. On duplicate submission, replay the stored response without re-executing.1874. Implement per-item result reporting: every result entry must include the input index or client-provided ID, the HTTP status for that item, and for failures, a machine-readable error code and human-readable message.1885. For best-effort batches with external side effects (email, webhooks, charges), document that failed items are not retried by the server and callers are responsible for resubmitting failures.189190## Harness Integration191192- **Type:** knowledge -- this skill is a reference document, not a procedural workflow.193- **No tools or state** -- consumed as context by other skills and agents.194- **related_skills:** api-idempotency-keys, api-error-contracts, api-http-methods, api-status-codes195196## Success Criteria197198- Bulk endpoints with best-effort semantics return `207 Multi-Status`, never `200 OK`, when any item fails.199- Per-item results in `207` responses include the input index or client-provided ID for every item, success or failure.200- Request size limits (item count and body bytes) are enforced and documented; violations return `400`/`413` with the limit values in the error body.201- Bulk create and bulk update endpoints accept and honor `Idempotency-Key` headers; duplicate submissions replay the stored response without re-executing.202- API documentation explicitly states whether semantics are transactional or best-effort for every bulk endpoint.