Skip to content
,

Claude Message Batches API Python Tutorial (2026): 50% Off, Limits, Polling and Caching

8–12 minutes
Laughing woman at a tech conference keynote, Claude logo card, hook 50% off claude batch api

The Claude Message Batches API runs thousands of requests in the background for half the normal price. If your job does not need an answer in seconds, it is the easiest 50% saving in the whole Claude API.

This Python tutorial covers the limits, a full create, poll and results script, error handling, and how to stack prompt caching on top for even lower bills.

TLDR

  • Discount: every batch token costs 50% of standard price, input and output.
  • Limits: up to 100,000 requests or 256 MB per batch. Most batches finish in under an hour, with a 24-hour maximum.
  • Results: download them for 29 days. They come back in any order, so match on custom_id.
  • Not allowed: streaming, fast mode (speed), server-side fallbacks and max_tokens: 0.
  • Stack savings: prompt caching works inside batches. Use the 1-hour cache for shared prefixes.

What the Message Batches API Is

A batch is a bundle of normal Messages API requests that you send in one call. Anthropic processes them asynchronously, then gives you a results file to download.

Each request in the batch can be different. You can mix models, system prompts, tools, images and multi-turn conversations in the same batch.

Quick answer: Use batches for any job where waiting minutes or hours is fine. You get the same models and the same quality for half the token price.

Good uses for batches include:

  • Bulk classification: tagging support tickets, reviews or emails.
  • Content work: summaries, translations or product descriptions at scale.
  • Evaluations: running a test set of prompts against a new model or prompt.
  • Data extraction: pulling fields out of thousands of documents.
  • Nightly jobs: anything that runs on a schedule with no user waiting.

Batch Pricing: 50% Off Every Model

All batch usage is charged at 50% of standard API prices. Here are the current batch rates for the main models, per million tokens.

Model Standard input / output Batch input / output
Claude Fable 5.2 $8 / $40 $4 / $20
Claude Opus 5.5 $4 / $20 $2 / $10
Claude Sonnet 5.5 $2 / $10 $1 / $5
Claude Haiku 5.5 (prompts up to 100K) $0.10 / $0.50 $0.05 / $0.25
Claude Haiku 5.5 (prompts over 100K) $0.50 / $2.50 $0.25 / $1.25

A quick cost example

Say you classify 10,000 tickets on Opus 5.5. Each request uses 1,500 input tokens and 300 output tokens.

  • Standard API: 15M input tokens x $4 = $60, plus 3M output tokens x $20 = $60. Total $120.
  • Batch API: the same tokens at half price. Total $60.
  • Batch on Haiku 5.5: 15M x $0.05 = $0.75, plus 3M x $0.25 = $0.75. Total $1.50, if Haiku is accurate enough for your task.

Only succeeded results are billed. Errored, canceled and expired requests cost nothing. For every other price on the Claude API, see our Claude API pricing guide.

Limits and Rules You Must Know

Batches have a few hard rules. Most “my batch failed” problems come from one of these.

Rule Value
Max requests per batch 100,000
Max batch size 256 MB (413 request_too_large if over)
Typical processing time Under 1 hour
Maximum processing time 24 hours, then unfinished requests expire
Results available for 29 days from created_at
custom_id format 1 to 64 characters: letters, numbers, _ and -, unique per batch
Batch API requests per minute 1,000 (Start), 2,000 (Build), 4,000 (Scale)
Requests waiting in the queue 200,000 (Start), 300,000 (Build), 500,000 (Scale)

These request parameters are rejected inside a batch:

  • stream: true: results come back as one file, not a stream.
  • speed: "fast": fast mode is for synchronous latency, so it does not apply. Our fast mode guide explains when that is worth paying for.
  • fallbacks: server-side refusal fallbacks are not supported. Retry refused items yourself.
  • max_tokens: 0: cache pre-warming does not work in batches.

Warning: Batches are not eligible for zero data retention or HIPAA. Anthropic stores inputs and outputs for up to 29 days so you can download results. Do not batch data that your policy says must never be stored.

Set Up Python

You need Python 3.10 or newer, the official SDK and an API key from the Claude Console.

pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-..."   # Windows PowerShell: $env:ANTHROPIC_API_KEY="sk-ant-..."

New to the SDK? Our Claude API Python tutorial covers keys, the first request and streaming.

Step by Step: Create, Poll and Read a Batch

  1. Test one request first. Send a single request through the normal Messages API. Batch validation errors only show up after the batch ends, so catch shape mistakes early.
  2. Build your request list. Wrap each request as a Request with a unique custom_id and a params object.
  3. Create the batch. Call client.messages.batches.create(requests=...) and save the returned batch ID.
  4. Poll the status. Call client.messages.batches.retrieve(id) every minute or so until processing_status is "ended".
  5. Stream the results. Loop over client.messages.batches.results(id) and key each result by custom_id.
  6. Handle failures. Fix and resend invalid_request_error items. Resend server errors and expired items as they are.
  7. Clean up. Save the results to your own storage, then delete the batch if you no longer need it on Anthropic’s side.

Create the batch

import anthropic
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request

client = anthropic.Anthropic()

batch = client.messages.batches.create(
    requests=[
        Request(
            custom_id="ticket-001",
            params=MessageCreateParamsNonStreaming(
                model="claude-opus-5-5",
                max_tokens=1024,
                messages=[{"role": "user", "content": "Classify this ticket: My invoice is wrong."}],
            ),
        ),
        Request(
            custom_id="ticket-002",
            params=MessageCreateParamsNonStreaming(
                model="claude-opus-5-5",
                max_tokens=1024,
                messages=[{"role": "user", "content": "Classify this ticket: The app crashes on login."}],
            ),
        ),
    ]
)

print(batch.id, batch.processing_status)  # msgbatch_..., in_progress

Poll until it ends

The batch moves through three states: in_progress, canceling and ended. The request_counts object shows how many requests are processing, succeeded, errored, canceled or expired.

import time

while True:
    batch = client.messages.batches.retrieve(batch.id)
    if batch.processing_status == "ended":
        break
    print("Still processing:", batch.request_counts.processing)
    time.sleep(60)

print(batch.request_counts)

Tip: Poll gently. Batch endpoints share one requests-per-minute limit across all models, so a tight loop over many batches can earn you 429 errors.

Read the results

Results arrive as JSONL, one object per request, in any order. Never match them by position.

for result in client.messages.batches.results(batch.id):
    outcome = result.result
    match outcome.type:
        case "succeeded":
            msg = outcome.message
            text = "".join(b.text for b in msg.content if b.type == "text")
            print(result.custom_id, "=>", text)
        case "errored":
            if outcome.error.error.type == "invalid_request_error":
                print("Fix and resend:", result.custom_id)
            else:
                print("Retry as is:", result.custom_id)
        case "canceled" | "expired":
            print("Resend later:", result.custom_id)

Full Working Script

This script classifies a list of support tickets, waits for the batch, saves results to a JSON file, and collects anything that needs a retry.

import json
import time
import anthropic
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request

client = anthropic.Anthropic()
MODEL = "claude-opus-5-5"  # try "claude-haiku-5-5" for cheap bulk labels

SYSTEM = (
    "You label customer support tickets. "
    "Reply with exactly one word: billing, bug, account, feature or other."
)

tickets = {
    "t-001": "I was charged twice this month.",
    "t-002": "The export button does nothing on Firefox.",
    "t-003": "Please add dark mode to the dashboard.",
}

def build_requests(items):
    return [
        Request(
            custom_id=tid,
            params=MessageCreateParamsNonStreaming(
                model=MODEL,
                max_tokens=256,
                system=SYSTEM,
                messages=[{"role": "user", "content": text}],
            ),
        )
        for tid, text in items.items()
    ]

def wait_for(batch_id, delay=30, max_delay=300):
    while True:
        b = client.messages.batches.retrieve(batch_id)
        if b.processing_status == "ended":
            return b
        time.sleep(delay)
        delay = min(delay * 2, max_delay)  # back off slowly

batch = client.messages.batches.create(requests=build_requests(tickets))
print("Created", batch.id)
batch = wait_for(batch.id)
print("Counts:", batch.request_counts)

labels, retry = {}, {}
for r in client.messages.batches.results(batch.id):
    if r.result.type == "succeeded":
        msg = r.result.message
        if msg.stop_reason == "refusal":
            retry[r.custom_id] = tickets[r.custom_id]
            continue
        labels[r.custom_id] = "".join(
            b.text for b in msg.content if b.type == "text"
        ).strip()
    else:
        retry[r.custom_id] = tickets[r.custom_id]

with open("labels.json", "w") as f:
    json.dump(labels, f, indent=2)

print("Saved", len(labels), "labels. Need retry:", list(retry))

To retry, call build_requests(retry) and send a new batch. Fix the request body first for any invalid_request_error items, or they will fail again.

Note: Fable 5.2, Opus 5.5 and Sonnet 5.5 reject forced tool_choice (any or a named tool) with a 400, and that rule applies inside batches too. For JSON output, use structured outputs instead. Our structured outputs tutorial shows how.

Combine Batches With Prompt Caching

The batch discount and prompt caching stack. If every request shares a long system prompt or document, caching that prefix cuts the input cost again.

The catch is timing. Batch requests run in parallel and in any order, so cache hits are best-effort. Anthropic reports typical hit rates between 30% and 98%.

How to get more cache hits

  • Use identical cache blocks: the cached prefix must match byte for byte in every request.
  • Use the 1-hour cache: batches often take 5 to 60 minutes, which is longer than the default 5-minute cache.
  • Warm the cache first: send one request with the shared prefix, wait for it to finish, then submit the rest.
  • Put shared content first: tools, then system, then messages. Anything that changes goes after the cache breakpoint.
LONG_DOC = open("handbook.txt").read()  # shared by every request

def cached_request(cid, question):
    return Request(
        custom_id=cid,
        params=MessageCreateParamsNonStreaming(
            model="claude-opus-5-5",
            max_tokens=1024,
            system=[
                {"type": "text", "text": "Answer using only the handbook below."},
                {
                    "type": "text",
                    "text": LONG_DOC,
                    "cache_control": {"type": "ephemeral", "ttl": "1h"},
                },
            ],
            messages=[{"role": "user", "content": question}],
        ),
    )

Check usage.cache_read_input_tokens in each succeeded message. If it stays at zero, something in your prefix is changing between requests. Prefixes shorter than 512 tokens on current models are not cached at all.

The 1-hour cache write costs 2x the base input price, while reads are much cheaper than base input. For a deeper look at the maths, read our prompt caching guide.

List, Cancel and Delete Batches

# List recent batches (auto-paginates)
for b in client.messages.batches.list(limit=20):
    print(b.id, b.processing_status, b.created_at)

# Cancel a running batch (ends with partial results)
client.messages.batches.cancel("msgbatch_...")

# Delete a finished batch and its stored data
client.messages.batches.delete("msgbatch_...")

A canceled batch moves to canceling, then ended. Requests already finished stay in the results. To delete a batch that is still running, cancel it first.

Batches belong to a Workspace. You only see batches created in the Workspace your API key belongs to.

Troubleshooting: Common Problems and Fixes

413 request_too_large

Your batch is over 256 MB. Split it into several smaller batches, or move large files to the Files API and reference them by ID.

Every result is errored with invalid_request_error

The request shape is wrong, for example a disabled thinking setting on Fable 5.2 or a forced tool_choice. Send one copy through the normal Messages API to see the exact error.

Many results expired

The batch hit the 24-hour limit before those requests ran, often during heavy demand. Resend the expired items in a new batch, and split very large jobs across the day.

Results do not match my inputs

Results come back in any order. Always look up the input by custom_id, never by list position.

Duplicate custom_id error

Each custom_id must be unique in a batch and match 1 to 64 letters, numbers, underscores or dashes. Use your own record IDs.

Cache reads are always zero

Something in the prefix changes, such as a timestamp in the system prompt. Keep the cached block identical, switch to the 1-hour cache, and warm it with one request first.

429 errors on batch endpoints

You are polling or creating batches too fast. Slow your loop and back off. Add exponential backoff to every retry.

Frequently Asked Questions

How much does the Claude Batch API save?

Batch requests cost 50% of standard prices for both input and output tokens. Fable 5.2 drops to $4 / $20 per million tokens, and Opus 5.5 to $2 / $10.

How long does a Claude batch take?

Most batches finish in under an hour. Any request not processed within 24 hours expires and is not billed.

How many requests can one batch hold?

Up to 100,000 requests or 256 MB, whichever comes first. Your tier also limits how many batch requests can wait in the queue at once.

Can I stream batch results?

You cannot set stream: true on batch requests. The SDK does stream the results file line by line, so large result sets do not fill your memory.

Can I use fast mode or Fable 5.2 in a batch?

Fable 5.2 works in batches at $4 / $20. Fast mode does not: a request with speed returns a validation error.

Do prompt caching and batch discounts stack?

Yes. Cache hits are best-effort in batches, so use identical cache blocks and the 1-hour cache duration for the best hit rate.

How long are batch results kept?

Results can be downloaded for 29 days after the batch was created. You can delete a finished batch sooner with the delete endpoint.

Sources

Free weekly email

Get new AI guides every week

One short weekly email with our newest local AI, ComfyUI and AI tool guides. No spam, no pop-ups, just the good stuff.

Free. Unsubscribe anytime in one click. We only use your email to send this newsletter. See our privacy policy.

Laughing woman at a tech conference keynote, Claude logo card, hook 50% off claude batch api