C.W.K.
Stream
Lesson 03 of 06 · published

Batch API Workflows in Production

~14 min · batch-api, async, evaluation

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

What batch is really for

The Batch API is the right tool for: nightly classification, periodic backfills, large evaluation runs, dataset annotation, content audits. Anything where 'the answer can land within 24 hours' is true. The trade is ~50% off per-token pricing for that flexibility.

Custom IDs are how you re-key results

Each batch request takes a custom_id. The results stream back keyed by that id, in arbitrary order. Map results back to your domain rows by custom_id, not by position. Treat the batch input list and output stream as two independent collections joined on the id.

Polling, webhooks, and idempotent retry

For light workloads, polling every 30-60 seconds is fine. For production, prefer webhooks (when offered) or scheduled checks. Always make the result-processing step idempotent — re-pulling a finished batch should not double-write your downstream store.

Principle: Batch is not 'slow API'. It is 'cheaper API for work that can wait'. Use the difference, do not pretend it is the same as Messages.

Code

Submit, poll, write idempotently·python
from anthropic import Anthropic
import time, json

client = Anthropic()

batch = client.messages.batches.create(
    requests=[
        {
            "custom_id": row["id"],
            "params": {
                "model": "claude-haiku-4-5-20251001",
                "max_tokens": 64,
                "system": "Reply with one word: positive, negative, or neutral.",
                "messages": [{"role": "user", "content": row["text"]}],
            },
        }
        for row in rows
    ]
)

while True:
    b = client.messages.batches.retrieve(batch.id)
    if b.processing_status == "ended":
        break
    time.sleep(60)

# Idempotent write: upsert by custom_id
for item in client.messages.batches.results(batch.id):
    label = item.result.message.content[0].text.strip()
    db.upsert("sentiment", {"id": item.custom_id, "label": label})
Per-row error handling·python
for item in client.messages.batches.results(batch.id):
    if item.result.type == "succeeded":
        save(item.custom_id, item.result.message)
    elif item.result.type == "errored":
        log.warning("batch row failed", id=item.custom_id, error=item.result.error)
        retry_queue.add(item.custom_id)

External links

Exercise

Move one nightly job to the Batch API. Add idempotent upsert on the result side. Run twice in a row from the same batch id and confirm rows do not double.
Hint
If your downstream store has unique constraints on custom_id, the upsert just falls out — design that way.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.