Claude Certification Guides
CCDV-F/Domain 28 min read

Lesson 2.5

The Message Batches API

Submitting thousands of Messages requests asynchronously at a discount, polling for completion, and reading results that arrive in any order.

The Message Batches API is the right tool when you have many independent requests and no user waiting on the other end. You submit them together, Anthropic processes them in the background within a 24-hour window, and you pay a substantial discount on both input and output tokens. The exam tests the lifecycle, the role of custom_id, and the trade-offs against real-time calls.

When to batch

Good fits: nightly classification of support tickets, bulk document summarisation, generating evaluation datasets, back-filling embeddings-style metadata, or any job measured in hours rather than seconds. Bad fits: chat, anything interactive, or workflows where step two depends on step one's output and you need it now. Batches support every Messages API feature, including tools, vision, system prompts and prompt caching, but not streaming.

Creating a batch

Each entry has a custom_id you choose and a params object that is exactly a Messages API request body.

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=f"ticket-{t['id']}",
            params=MessageCreateParamsNonStreaming(
                model=MODEL, max_tokens=200,
                system="Classify the ticket as billing, technical or general.",
                messages=[{"role": "user", "content": t["body"]}]))
    for t in tickets
])
print(batch.id, batch.processing_status)      # msgbatch_..., in_progress
const batch = await client.messages.batches.create({
  requests: tickets.map((t) => ({
    custom_id: `ticket-${t.id}`,
    params: {
      model: MODEL, max_tokens: 200,
      system: "Classify the ticket as billing, technical or general.",
      messages: [{ role: "user", content: t.body }],
    },
  })),
});

custom_id must be unique within the batch. It is the only way to match a result back to its request, because results are not returned in submission order. Batches are capped by request count and total payload size; the current limits are on the batch processing page of docs.anthropic.com.

Polling for completion

The batch object exposes processing_status (in_progress, canceling, ended) and request_counts with processing, succeeded, errored, canceled and expired tallies.

import time
while (b := client.messages.batches.retrieve(batch.id)).processing_status != "ended":
    print(b.request_counts.processing, "left")
    time.sleep(60)

Poll on the order of a minute, not a second. Most batches finish well inside an hour; the guarantee is only that processing stops within 24 hours, after which unfinished requests are marked expired and are not billed.

Reading results

Results are streamed as JSON Lines from a results_url. The SDK gives you an iterator.

for item in client.messages.batches.results(batch.id):
    match item.result.type:
        case "succeeded":
            msg = item.result.message
            text = next(b.text for b in msg.content if b.type == "text")
            store(item.custom_id, text, msg.usage)
        case "errored":
            log_error(item.custom_id, item.result.error)     # fix and resubmit
        case "expired" | "canceled":
            requeue(item.custom_id)
for await (const item of await client.messages.batches.results(batch.id)) {
  if (item.result.type === "succeeded") {
    const msg = item.result.message;  // a normal Message with content, stop_reason, usage
  }
}

Each successful result contains a complete Message object, so stop_reason, usage and content blocks are read exactly as in a synchronous call. Results stay available for 29 days after creation.

Cancelling and listing

client.messages.batches.cancel(id) moves the batch to canceling; requests already processed still return results and are billed, the rest become canceled. client.messages.batches.list() pages through your batches and is useful for operational dashboards.

Cost and caching

Batch pricing is a flat percentage discount on standard token prices (historically 50%), on top of which prompt caching still applies: a shared system block with cache_control across thousands of requests can be served from cache for most of them, provided the requests land close enough together. Check the pricing page for the current multiplier rather than hard-coding it.

Operational patterns

  • Persist batch.id and the mapping from custom_id to your own record IDs before you start polling, so a crashed worker can resume.
  • Keep custom_id deterministic (for example a database primary key) so re-submission is idempotent on your side.
  • Split enormous jobs into several batches to stay within limits and to get partial results sooner.
  • Validate request bodies locally first; an errored result with invalid_request wastes a 24-hour cycle.

Exam focus

  • Batches are asynchronous, complete within 24 hours, and are priced at a discount to real-time requests; results are kept for 29 days.
  • Each request needs a unique custom_id; results arrive in arbitrary order and must be matched by that ID.
  • processing_status moves from in_progress to ended; per-request outcomes are succeeded, errored, canceled or expired.
  • A succeeded result wraps a normal Message with content, stop_reason and usage.
  • All Messages API features except streaming are supported, and prompt caching still applies inside a batch.
  • Use batches for bulk, non-interactive work; never for chat or latency-sensitive paths.

Check yourself

5 questions

  1. 1.What is the fundamental trade-off of the Message Batches API compared with the Messages API?

  2. 2.This batch creation call fails with a 400: client.messages.batches.create({ requests: [ { params: { model, max_tokens: 500, messages: [...] } }, { params: { model, max_tokens: 500, messages: [...] } } ] }); What is missing?

  3. 3.A developer collects batch results with: const results = []; for await (const r of await client.messages.batches.results(batchId)) results.push(r); results.forEach((r, i) => save(inputs[i].id, r)); Some outputs are saved against the wrong input. Why?

  4. 4.A batch processing loop only handles result.type === "succeeded" and drops everything else. Which statement about the other result types is accurate?

  5. 5.How does a client know that a message batch has finished processing?