The Claude Message Batches API lets you submit up to 100,000 Messages requests as one asynchronous job and pays 50 percent of the standard price for all usage. You create the batch, poll until its processing_status is ended, then stream the results and match them to your requests by custom_id. It suits work where nobody is waiting for an answer: evaluations, classification, summarisation and bulk content generation.
Who this is for and what you will have
This guide is for developers who already call Claude with the Python SDK and now have a large queue of independent prompts, such as thousands of documents to classify or summarise. By the end you will have a working Python script that builds a batch from a list of inputs, waits for it, writes results to disk, separates failures for retry, and respects the documented limits. It also covers when a batch is the wrong tool.
When to use batches
A batch trades latency for price. Choose it when all of these are true:
- Each request is independent of the others.
- You do not need an immediate response; hours are acceptable.
- You have enough volume that a 50 percent discount matters.
Typical fits listed in the documentation are large-scale evaluations, content moderation, data analysis over large datasets and bulk content generation. Anything interactive, such as a chat window, should keep using the normal Messages API.
Prerequisites
- An Anthropic API key in the
ANTHROPIC_API_KEYenvironment variable. - The Python SDK:
pip install anthropic. - A request shape that already works on the normal Messages API. Validation of each request's
paramshappens asynchronously, and errors are only reported when the whole batch has ended, so test one request synchronously first.
Limits and pricing you need to know
| Item | Value from the documentation |
|---|---|
| Requests per batch | 100,000 |
| Size per batch | 256 MB; whichever limit is reached first applies |
| Processing time | Most batches finish within 1 hour; a batch expires if not complete within 24 hours |
| Result availability | 29 days after the batch is created |
| Scope | A Workspace in the Claude Console |
custom_id | 1 to 64 characters, matching ^[a-zA-Z0-9_-]{1,64}$, unique within the batch |
max_tokens | At least 1 on each request |
| Discount | 50 percent of standard API prices on all usage |
Batch prices are half of standard prices. At the time of writing the pricing page lists these batch rates, among others:
| Model | Batch input per million tokens | Batch output per million tokens |
|---|---|---|
| Claude Opus 5.5 | $2 | $10 |
| Claude Sonnet 5.5 | $1 | $5 |
| Claude Haiku 5.5 (prompts up to 100,000 tokens) | $0.05 | $0.25 |
Prices change, so confirm the current figures on the pricing page before you budget a job. All active models support the Batches API.
Two cautions from the documentation: rate limits apply both to the HTTP requests you make to the Batches API and to the number of requests waiting to be processed, and because of concurrent processing a batch can go slightly over the spend limit set for your Workspace.
What cannot go in a batch
Most Messages features work in a batch, including vision, tool use, system messages, multi-turn conversations and extended thinking. These parameters are rejected with a validation error:
stream: true, because results come back as one file.speed(fast mode), which targets synchronous latency.fallbacks(server-side fallback).max_tokens: 0, which is used for cache pre-warming.
Step 1: Build and submit a batch
Each request has a custom_id and a params object holding ordinary Messages parameters. The following script classifies a list of support tickets.
import anthropic
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request
client = anthropic.Anthropic()
tickets = {
"ticket-1001": "My invoice shows the wrong VAT number.",
"ticket-1002": "I cannot sign in after changing my phone.",
"ticket-1003": "How do I export my data to CSV?",
}
requests = [
Request(
custom_id=ticket_id,
params=MessageCreateParamsNonStreaming(
model="claude-sonnet-5-5",
max_tokens=100,
system="Classify the ticket as billing, access or how-to. Reply with one word.",
messages=[{"role": "user", "content": text}],
),
)
for ticket_id, text in tickets.items()
]
batch = client.messages.batches.create(requests=requests)
print(batch.id, batch.processing_status)The first response has processing_status set to in_progress, a request_counts object with every request under processing, an expires_at 24 hours after creation, and results_url set to null. Save the batch id; you need it to poll and fetch results.
Use meaningful custom_id values such as the ticket number. The documentation states that result order is not guaranteed, so the ID is the only reliable way to match a result to its input.
Step 2: Poll until the batch ends
The processing_status starts as in_progress and becomes ended once every request has finished and results are ready. You can also watch batches in the Claude Console under your Workspace's batches page.
import time
while True:
batch = client.messages.batches.retrieve(batch.id)
if batch.processing_status == "ended":
break
print(f"{batch.id}: {batch.request_counts}")
time.sleep(60)
print(batch.request_counts)A 60-second sleep matches the documentation's own example. Because batches can run for hours, in production you would store the batch ID in a database and poll from a scheduled job rather than keep a process running.
To see earlier batches in the Workspace, use the list endpoint, client.messages.batches.list(). Batches belong to the Workspace the API key runs in, so a key from another Workspace will not see them.
Step 3: Read the results
Once the batch has ended, each request has exactly one of four result types.
| Result type | Meaning | Billed? |
|---|---|---|
succeeded | The request worked and includes the message | Yes |
errored | Invalid request or internal server error; no message was created | No |
canceled | You canceled the batch before this request was sent to the model | No |
expired | The 24-hour limit passed before this request was sent to the model | No |
Stream the results rather than downloading everything at once; the documentation recommends this because result files can be large. Results are served as JSON Lines.
import json
succeeded = {}
retry_later = []
fix_first = []
for entry in client.messages.batches.results(batch.id):
outcome = entry.result
if outcome.type == "succeeded":
succeeded[entry.custom_id] = outcome.message.content[0].text
elif outcome.type == "errored":
if outcome.error.error.type == "invalid_request_error":
fix_first.append(entry.custom_id) # fix the request body before re-sending
else:
retry_later.append(entry.custom_id) # server error, can be retried directly
elif outcome.type == "expired":
retry_later.append(entry.custom_id)
with open("results.json", "w") as f:
json.dump(succeeded, f, indent=2)
print(len(succeeded), "succeeded;", len(retry_later), "to retry;", len(fix_first), "to fix")The documentation distinguishes two error classes: an invalid_request_error means the request body must be corrected before it is sent again, while other server errors can be retried as they are. Note that one failed request does not affect the others in the batch.
To retry, build a new list containing only the failed custom_id values and submit it as a second batch. A batch cannot be edited after submission.
Step 4: Cancel or delete a batch
To stop a running batch, call the cancel endpoint. The status changes to canceling immediately, then to ended after finalization, and the results may contain partial output for requests processed before cancellation. Cancellation might not take effect instantly.
client.messages.batches.cancel(batch.id)Batch processing stores both inputs and outputs for up to 29 days after creation. You can delete a batch at any time after processing with DELETE /v1/messages/batches/{batch_id}; to delete an in-progress batch, cancel it first.
Combining batches with prompt caching
Batch pricing and prompt caching discounts can stack. If every request starts with the same large system prompt or reference document, mark it with cache_control in each request. Because batch requests run concurrently and in any order, cache hits are best-effort, and the documentation says users typically see hit rates between 30 and 98 percent depending on traffic.
To raise the hit rate:
- Put identical
cache_controlblocks in every request. - Keep requests flowing steadily so entries do not expire after the default 5-minute lifetime.
- Share as much cached content between requests as possible.
Because batches can take longer than five minutes, the documentation suggests the 1-hour cache duration for batches with shared context. See Anthropic prompt caching: cut cost and latency for the TTL pricing multipliers and minimum prompt lengths.
Troubleshooting
413 request_too_large when creating a batch
The total request size exceeded 256 MB. Split the work into several smaller batches. The documentation also recommends smaller batches for manageability even when under the limit.
Every request ends as errored with invalid_request_error
Validation runs asynchronously, so you only learn about a malformed params object after the batch ends. Send one request through the regular Messages API first and compare its shape. Check that none of the unsupported parameters (stream, speed, fallbacks, max_tokens: 0) are present and that max_tokens is at least 1.
Requests show as expired
The batch did not finish within 24 hours. Processing can slow when demand or your request volume is high. Resubmit only the expired custom_id values, possibly in smaller batches. Expired requests are not billed.
Results are gone
Results are available for 29 days after the batch created_at time, not after ended_at. After that you can view the batch but not download results. Download and store results soon after a batch ends.
Duplicate or invalid custom_id
Each custom_id must be unique in the batch and match ^[a-zA-Z0-9_-]{1,64}$. Spaces, dots and slashes are not allowed, so replace them or use a hash of the original identifier.
Cannot see a batch someone else created
Batches are isolated to a Workspace. Use an API key from the same Workspace, or a Console user with permission to view the Workspace's batches. Downloading results in the Console can also be disabled at the organization or Workspace level.
Checklist
- Confirm the work is asynchronous and independent per request.
- Test one request on the Messages API before batching.
- Use unique, meaningful
custom_idvalues, because order is not guaranteed. - Keep each batch under 100,000 requests and 256 MB.
- Poll
processing_statusuntil it isended, then stream results. - Handle
succeeded,errored,canceledandexpiredseparately and resubmit failures in a new batch. - Download results within 29 days of batch creation.
- Use identical
cache_controlblocks and the 1-hour cache if requests share a large prefix. - Check current batch prices for your model before estimating a budget.
References
- Batch processing (Claude API docs): https://platform.claude.com/docs/en/build-with-claude/batch-processing
- Message Batches API reference: https://platform.claude.com/docs/en/api/messages/batches/create
- Message Batches API rate limits: https://platform.claude.com/docs/en/api/rate-limits#message-batches-api
- Prompt caching: https://platform.claude.com/docs/en/build-with-claude/prompt-caching
Comments
Questions and tips are welcome. Comments are checked by the EpiFive team before they appear; please do not share passwords, phone numbers or other private details.