AI engineering

Claude Message Batches API: Cut Bulk Job Costs in Python

Run large asynchronous Claude jobs at 50 percent of standard API prices with the Message Batches API: Python create, poll and results code, limits and fixes.

By EpiFive Editorial TeamReviewed/updated 9 min readHow we check articles
On this page

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_KEY environment variable.
  • The Python SDK: pip install anthropic.
  • A request shape that already works on the normal Messages API. Validation of each request's params happens 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

ItemValue from the documentation
Requests per batch100,000
Size per batch256 MB; whichever limit is reached first applies
Processing timeMost batches finish within 1 hour; a batch expires if not complete within 24 hours
Result availability29 days after the batch is created
ScopeA Workspace in the Claude Console
custom_id1 to 64 characters, matching ^[a-zA-Z0-9_-]{1,64}$, unique within the batch
max_tokensAt least 1 on each request
Discount50 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:

ModelBatch input per million tokensBatch 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 typeMeaningBilled?
succeededThe request worked and includes the messageYes
erroredInvalid request or internal server error; no message was createdNo
canceledYou canceled the batch before this request was sent to the modelNo
expiredThe 24-hour limit passed before this request was sent to the modelNo

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:

  1. Put identical cache_control blocks in every request.
  2. Keep requests flowing steadily so entries do not expire after the default 5-minute lifetime.
  3. 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_id values, because order is not guaranteed.
  • Keep each batch under 100,000 requests and 256 MB.
  • Poll processing_status until it is ended, then stream results.
  • Handle succeeded, errored, canceled and expired separately and resubmit failures in a new batch.
  • Download results within 29 days of batch creation.
  • Use identical cache_control blocks and the 1-hour cache if requests share a large prefix.
  • Check current batch prices for your model before estimating a budget.

References

Questions people ask

How much cheaper is the Claude Message Batches API?

All usage in a batch is charged at 50 percent of standard API prices, for input tokens, output tokens and special tokens alike. For example, the documentation lists Claude Sonnet 5.5 at 1 dollar per million input tokens and 5 dollars per million output tokens in batch mode.

How long does a Claude batch take to finish?

Most batches finish in less than an hour, but a batch can take up to 24 hours. If processing is not complete within 24 hours the batch expires, and requests that were not yet sent to the model are marked expired and are not billed.

What is the maximum size of a Message Batch?

A batch is limited to 100,000 Message requests or 256 MB, whichever is reached first. A batch above the size limit can return a 413 request_too_large error, so split very large datasets into several batches.

How long can I download batch results?

Results are available for 29 days after the batch is created, not after it finishes. After that you can still view the batch, but its results can no longer be downloaded.

Claude APIMessage BatchesPythonAnthropic
  1. Claude Structured Outputs: Get Valid JSON from the API in Python

    Use output_config with a JSON schema, or messages.parse with Pydantic, to get schema-valid JSON from Claude in Python, plus the schema limits and failure cases to handle.

    AI engineering9 min read
  2. LLM Model Routing: Send Easy Requests to a Cheaper Model

    Build a simple Python router that sends each request to a small or large Claude model, then measure cost and quality against an eval set before you ship it.

    AI engineering12 min read
  3. Getting Started with LLM Evals: A Small-Team Setup with Code

    Build your first LLM eval in an afternoon: write 30 test cases in a JSONL file, grade them with simple code, add an LLM grader for tone, and track the score between prompt changes.

    AI engineering9 min read
  4. Anthropic Prompt Caching: Cut Cost and Latency on Repeated Context

    Use Claude prompt caching to cut input cost on repeated prompts: cache_control placement, 5-minute and 1-hour TTLs, pricing maths, minimum lengths and fixes.

    AI engineering9 min read
  5. Claude Tool Use in Python: Build a Simple Agent Loop Step by Step

    Build a tool-calling agent loop with the Claude Messages API in Python: define tools, run tool_use blocks, return tool_result blocks and stop safely.

    AI engineering11 min read
  6. LLM API Timeouts, Retries and Rate Limits: A Python Reliability Guide

    Make Claude and OpenAI API calls in Python resilient: set timeouts, retry only the right errors, honor retry-after on 429, and tell spend limits from rate limits.

    AI engineering10 min read