Check Batch Status

Su Suriya Ravichandran Updated 10 Nov 2026
8 min read ·Lesson 134 of 200

Following a Batch from Submission to Completion

After a batch is submitted, it moves through a small set of states, and it does not tell you anything about its progress in real time. You learn where it stands by asking. The API provides a retrieve call for a single batch, which returns its current state, and the answer you get back is what your application uses to decide what to do next.

This lesson explains each state, what the counts mean, how to check status without wasting requests, and how to build a waiting function that stops at a time you choose. The examples work offline, using a fake client that behaves like the real one, so you can see the full lifecycle without any cost.


The Three Processing States

Every batch has a processing_status, which takes one of three values:

StatusMeaningWhat you can do
in_progressThe batch is being processedKeep waiting, or check the counts for progress
cancelingA cancellation was requested, and the batch is winding downWait for it to reach ended
endedProcessing is complete, and results are readyRead the results file

The status changes only in one direction: in_progress may move to canceling or ended, and canceling moves to ended. A batch never goes back. That one-way movement makes polling simple: once you see ended, you can stop checking.

Notice that ended does not mean every request succeeded. It means every request has reached a final outcome, and the outcomes are recorded in the counts and in the results file.


Reading the Request Counts

The request_counts object breaks the batch down by outcome:

CountMeaning
processingRequests still being worked on
succeededRequests that produced a message
erroredRequests that failed with an error, and produced no message
canceledRequests that were canceled before reaching the model
expiredRequests that reached the 24-hour expiry before being sent to the model

Two properties follow from the documentation. First, the counts always add up to the total number of requests in the batch. Second, all counts except processing stay at zero until the batch has ended. So during processing, processing tells you how much work is left, but succeeded and the others tell you nothing yet.

The last two outcomes are not billed. Requests that errored, were canceled, or expired were never processed by the model, so they cost nothing. Only succeeded requests carry token charges.


The Timestamps

A batch object includes several times, and each one answers a different question:

FieldMeaning
created_atWhen the batch was submitted
expires_atWhen processing stops, 24 hours after creation
ended_atWhen processing finished, or null while it is still running
cancel_initiated_atWhen a cancellation was requested, or null
results_urlThe location of the results file, present only after the batch has ended

expires_at is a deadline, not a prediction. A batch that is still in_progress at that moment is cut off, and its unfinished requests become expired. Reading expires_at when you submit a batch lets you set your own deadline earlier, which gives you time to react.


Polling Without Wasting Requests

The simplest approach is a loop that retrieves the batch and sleeps, as the documentation's example does. That works, but the interval matters. Polling every second produces many requests for a process that takes an hour. Polling only once a day misses results for a day.

A sensible approach waits longer between checks as time passes, and never waits longer than a fixed maximum. Short early intervals catch quick batches, and longer later intervals keep the request count low for slow ones:

def next_interval(elapsed: float, first: float = 30, cap: float = 300) -> float:
    """Wait longer as the batch runs longer, but never longer than the cap."""
    if elapsed < 300:
        return first
    if elapsed < 1800:
        return 120
    return cap

With these values, the checks happen at roughly 30-second intervals for the first five minutes, every two minutes until the thirtieth minute, and every five minutes after that. A batch that finishes in ten minutes is checked about twenty times, while a batch that takes an hour is checked fewer than twenty times.


Practical Example: A Waiting Function

The function below waits for a batch to end. It accepts a callback so the caller can report progress, a time limit so the wait cannot run forever, and an injectable sleep function so the logic can be tested without real delays.

"""batch_status.py: wait for a batch to end, with progress reports and a time limit."""

import time
from dataclasses import dataclass


@dataclass
class Snapshot:
    status: str
    processing: int
    succeeded: int
    errored: int
    canceled: int
    expired: int
    elapsed: float


def snapshot_of(batch, elapsed: float) -> Snapshot:
    counts = batch.request_counts
    return Snapshot(batch.processing_status, counts.processing, counts.succeeded,
                    counts.errored, counts.canceled, counts.expired, elapsed)


def next_interval(elapsed: float, first: float = 30, cap: float = 300) -> float:
    if elapsed < 300:
        return first
    if elapsed < 1800:
        return 120
    return cap


def wait_for_batch(client, batch_id: str, *, max_seconds: float = 24 * 3600,
                   on_progress=None, sleep=time.sleep, clock=time.monotonic):
    """Return the batch once it has ended, or raise TimeoutError if max_seconds passes first."""
    start = clock()
    while True:
        elapsed = clock() - start
        batch = client.messages.batches.retrieve(batch_id)
        snap = snapshot_of(batch, elapsed)
        if on_progress:
            on_progress(snap)
        if batch.processing_status == "ended":
            return batch
        if elapsed >= max_seconds:
            raise TimeoutError(f"batch {batch_id} is still {batch.processing_status} "
                               f"after {elapsed:.0f} seconds")
        sleep(min(next_interval(elapsed), max_seconds - elapsed))

How It Works

  1. snapshot_of() copies the fields the caller cares about into a plain object. Reporting code never has to know the SDK's classes.
  2. wait_for_batch() retrieves first, then decides. The first call happens immediately, so an already-finished batch returns at once.
  3. The progress callback runs on every check, so a dashboard or log always shows the latest counts.
  4. The loop returns only on ended. Canceling batches are not yet finished, so the loop keeps waiting for ended, which matches the documented sequence.
  5. The time limit is checked before sleeping. The sleep never runs past the limit, so the timeout is accurate to within one check.
  6. sleep and clock are parameters, which is what makes the demonstration below possible without waiting.

Demonstration with a Fake Client

The fake client below returns a fixed sequence of states, so you can watch the lifecycle without submitting anything. It is not a substitute for real tests, but it shows exactly how the function responds to each state.

"""batch_status_demo.py: watch a batch move through its states, using a fake client."""

from types import SimpleNamespace

from batch_status import wait_for_batch


def fake_batch(status, processing, succeeded=0, errored=0):
    return SimpleNamespace(
        id="msgbatch_demo",
        processing_status=status,
        request_counts=SimpleNamespace(processing=processing, succeeded=succeeded,
                                       errored=errored, canceled=0, expired=0),
    )


class FakeBatches:
    def __init__(self, sequence):
        self.sequence = list(sequence)

    def retrieve(self, batch_id):
        return self.sequence.pop(0) if len(self.sequence) > 1 else self.sequence[0]


client = SimpleNamespace(messages=SimpleNamespace(batches=FakeBatches([
    fake_batch("in_progress", processing=3),
    fake_batch("in_progress", processing=1, succeeded=2),
    fake_batch("ended", processing=0, succeeded=2, errored=1),
])))

ticks = iter(range(0, 1000, 60))          # Each check is 60 seconds later
seen = []
batch = wait_for_batch(client, "msgbatch_demo",
                       on_progress=lambda s: seen.append((s.status, s.processing, s.succeeded)),
                       sleep=lambda seconds: None,
                       clock=lambda: next(ticks))

for status, processing, succeeded in seen:
    print(f"{status:<12} processing={processing} succeeded={succeeded}")
print("final:", batch.processing_status)

Output:

in_progress  processing=3 succeeded=0
in_progress  processing=1 succeeded=2
ended        processing=0 succeeded=2
final: ended

How It Works

  1. The fake client replays three states, the way a real batch moves from submission to completion.
  2. The fake clock advances by 60 seconds per check, so the time limit logic runs without real waiting.
  3. Each progress report records the state the function saw, so the printed table shows the sequence in order.
  4. The function returns the final batch object when it sees ended, which is the object the next lesson reads results from.

Note: the fake client returns the last state repeatedly once the sequence is exhausted. This matches how a real batch behaves at the end: once ended, it stays ended.


Checking Without Waiting

Not every program needs to block. A dashboard or a scheduled job may only want to know the current state. For that, a single retrieve call is enough, and the function below turns the state into a plain message:

def describe(batch) -> str:
    counts = batch.request_counts
    if batch.processing_status == "ended":
        return (f"ended: {counts.succeeded} succeeded, {counts.errored} errored, "
                f"{counts.canceled} canceled, {counts.expired} expired")
    return f"{batch.processing_status}: {counts.processing} still processing"

A scheduled job can call describe() every hour, and write the result to its log. The job never blocks, and the log shows progress over the batch's lifetime.


The Retrieve Call Is Idempotent

Retrieving a batch changes nothing on the server. Calling it ten times returns the same answer, so checking status is always safe to repeat, including after a crash or a restart. Your program can store the batch ID, exit, and check again later, without any risk of duplicating the work.


Summary

A batch is a long-running object with three states and a set of counts. Waiting for it means retrieving it repeatedly, with intervals that grow over time and a clear time limit. The counts reveal progress only while processing, and the timestamps set your deadlines. Once a batch has ended, its outcomes are final, and the next step is reading its results.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Check Batch Status and get answers drawn from it.

Signed-in readers only.