Tools

Message Batches API: cut Claude costs by 50% for work that can wait

TL;DR

The Batch API accepts up to 100,000 requests (or 256 MB) per batch, billed at 50% of standard prices. Most batches finish in under an hour, at most 24 hours; results are kept for 29 days. It stacks with prompt caching.

Not every request you send to Claude needs an answer within seconds. Classifying 10,000 support tickets, summarising a document archive, running an eval suite across prompt versions, generating descriptions for thousands of products — all of this can wait. For that kind of work, the Message Batches API lets you submit a whole batch at once, processes it asynchronously, and charges just 50% of standard prices.

What is the Batches API?

It is the Messages API called in bulk: you bundle many requests into a batch, submit it, and come back for the results when processing ends. Each request is handled independently — one failed request doesn’t affect the rest. According to the official docs, most batches finish in under 1 hour.

Numbers to remember

ItemLimit
Price50% of standard API prices, for both input and output
Max requests per batch100,000 requests
Max batch size256 MB (whichever limit is hit first applies; exceeding it returns 413 request_too_large)
Processing timeMostly under 1 hour; up to 24 hours — anything left after that is expired
Result retention29 days from created_at (not from when it ended)
custom_idUnique within the batch, 1–64 letters, digits, _ or -

Reference batch prices (USD per million tokens)

ModelInput (batch)Output (batch)
Claude Opus 5.5$2$10
Claude Sonnet 5.5$1$5
Claude Haiku 5.5 (prompts ≤100K tokens)$0.05$0.25

Prices from the Claude Docs batch-processing page at the time of writing; always re-check before budgeting.

What is and isn’t supported

All active models support the Batches API. A single batch can mix request types, and nearly every Messages API feature works: vision, tool use (including server tools such as web search, web fetch, code execution and MCP connectors), system prompts, multi-turn conversations, extended thinking, most beta features, and prompt caching.

Parameters that fail validation:

  • stream: true — results come back as a file, not a stream.
  • speed (fast mode) and fallbacks — only meaningful for the synchronous API.
  • max_tokens: 0 (cache pre-warming) — the cache would expire before a follow-up request could use it.

One nice bonus: with the beta header output-300k-2026-03-24, batch requests can raise max_tokens to 300,000 on supported models — available on the Batches API only, not the synchronous API.

The 4-step workflow (Python)

Step 1 — Create the batch

import anthropic
client = anthropic.Anthropic()

batch = client.messages.batches.create(requests=[
    {
        "custom_id": f"ticket-{t['id']}",
        "params": {
            "model": "claude-haiku-5-5",
            "max_tokens": 200,
            "system": "Classify the ticket as: billing, bug, feature, other. Return only the label.",
            "messages": [{"role": "user", "content": t["text"]}],
        },
    }
    for t in tickets
])
print(batch.id, batch.processing_status)  # in_progress

Note: params are validated asynchronously — errors only show up once the batch ends. So dry-run one identical request through the regular Messages API before submitting the whole batch.

Step 2 — Poll for status

import time
while True:
    b = client.messages.batches.retrieve(batch.id)
    if b.processing_status == "ended":
        break
    print(b.request_counts)  # processing / succeeded / errored / canceled / expired
    time.sleep(60)

processing_status takes three values: in_progress, canceling and ended. You can also watch batches in the Claude Console.

Step 3 — Fetch the results

labels = {}
retry = []
for r in client.messages.batches.results(batch.id):
    match r.result.type:
        case "succeeded":
            labels[r.custom_id] = r.result.message.content[0].text
        case "errored":
            if r.result.error.error.type == "invalid_request_error":
                print("Fix request:", r.custom_id)
            else:
                retry.append(r.custom_id)   # server error: safe to resend
        case "expired" | "canceled":
            retry.append(r.custom_id)

Results are a .jsonl file, one object per line with custom_id and result. Crucially, results don’t come back in submission order — always match them by custom_id.

Result typeMeaningBilled?
succeededSuccess, includes the messageYes
erroredInvalid request or server errorNo
canceledCancelled before reaching the modelNo
expiredThe 24-hour window ran out firstNo

Step 4 — Cancel or delete when needed

Call client.messages.batches.cancel(id) to cancel a running batch: its status moves to canceling, then ended, possibly with partial results. To delete batch data after processing, use DELETE /v1/messages/batches/{batch_id} (cancel an in-progress batch first).

Combining with prompt caching

Batch and caching discounts stack. Because batch requests run concurrently, cache hits are best-effort, typically between 30% and 98%. To raise the hit rate:

  1. Put identical cache_control blocks in every request (for example the system prompt plus reference documents).
  2. Keep a steady request stream so 5-minute cache entries don’t expire mid-run.
  3. Structure requests to share as much content as possible; for long-running batches, consider the 1-hour cache.

When to use it — and when not to

Good fitPoor fit
Bulk classification, labelling, data extractionChatbots answering users in real time
Summarising or translating document archivesMulti-step agents that need each step’s output to continue
Evals, comparing many prompt versionsWork with second-level deadlines
Content moderation, high-volume generationRequests that need token streaming

Operational notes

  • Split it up: break very large datasets into several batches so they’re easier to monitor and retry.
  • Stream results: iterate over results() instead of loading everything into memory.
  • Spend limits: because of the high throughput, a batch may slightly exceed the Workspace spend limit.
  • Workspace scope: a batch belongs to the Workspace that created it; only API keys from that Workspace or Console users with permission can see it.
  • Data: requests and responses are stored for up to 29 days for processing; delete proactively via the DELETE endpoint if the data is sensitive.

How to apply it

  • Developers: Split your pipeline into two layers — real time (regular API) for users, and an overnight batch layer for analysis, labelling and reports. The bill drops noticeably while the user experience stays the same.
  • Data/QA: Run thousand-case eval suites as batches every time a prompt changes — half the price and no pressure on your synchronous rate limits.
  • PMs: When estimating AI costs for a feature, ask the team: “which part can wait a few hours?” Moving that part to batches saves 50% immediately.

Sources & references

  1. Batch processing — Claude Docs
  2. Message Batches API reference — Claude Docs