List Message Batches

Su Suriya Ravichandran Updated 10 Nov 2026
7 min read ·Lesson 135 of 200

Seeing Every Batch in Your Workspace

Lessons 131–134 worked with one batch at a time, because each submission returned an ID and you kept that ID. In a real system, dozens or thousands of batches accumulate: nightly jobs, ad-hoc experiments, retries, and batches submitted by colleagues. Some of them finish, some fail, and some are still running. Listing batches lets you see the whole picture, rebuild your own records after a crash, and find work that has gone missing.

The list endpoint returns batches in your Workspace. Batches are scoped to the Workspace in which they were created, so the list shows the batches in that Workspace, and only batches within the 29-day retention window are available for download (Lesson 136). Older batches still appear in the Console, but their results can no longer be retrieved.


The Basic Call

The SDK exposes listing as a paginated iterator. You do not request pages yourself; you iterate, and the SDK fetches more pages as needed:

from anthropic import Anthropic

client = Anthropic()

for batch in client.messages.batches.list(limit=20):
    print(batch.id, batch.processing_status, batch.created_at)

The limit argument controls how many items each underlying request asks for. It does not limit the total, because the iterator keeps fetching pages until the list is exhausted. To stop early, break out of the loop, which is what the examples below do.

Each item in the list is a full batch object, with the same fields Lesson 134 described, including request_counts, expires_at, and results_url. A list therefore gives you everything you need to report on many batches without retrieving each one separately.


Pagination and Stopping Early

A workspace with a long history can contain many thousands of batches. Loading them all just to find the recent ones is wasteful. Because the iterator is lazy, it requests pages only as you consume items, so stopping early keeps the number of requests small:

def latest_batches(client, count: int = 10) -> list:
    found = []
    for batch in client.messages.batches.list(limit=20):   # Newest first, page by page
        found.append(batch)
        if len(found) == count:
            break
    return found

The limit of 20 means each page holds up to 20 batches, so ten results usually need only one request. Increasing limit reduces the number of requests for long scans, but each request returns more data. For most reporting, a value between 20 and 100 is a reasonable compromise.


Summarizing a Workspace

A single list call can feed a summary of the workspace's recent activity. The example below groups batches by status and totals their requests, which is the kind of report a team uses to see whether anything is stuck:

from collections import Counter


def summarize(batches) -> dict:
    by_status = Counter(batch.processing_status for batch in batches)
    totals = Counter()
    for batch in batches:
        counts = batch.request_counts
        totals.update({"succeeded": counts.succeeded, "errored": counts.errored,
                       "canceled": counts.canceled, "expired": counts.expired})
    return {"batches": len(batches), "by_status": dict(by_status), "requests": dict(totals)}

Because each batch object already contains its counts, the summary needs no extra requests beyond the list itself.


Reconciling Your Records with the Server

Your application keeps its own record of every batch it submits, usually in a registry like the one from Lesson 133. The server has its own list. Reconciliation compares the two, and finds three kinds of disagreement:

SituationWhat it meansWhat to do
In your registry, not on the serverThe batch was deleted, or it fell outside the visible windowInvestigate, and mark the local record accordingly
On the server, not in your registryA batch was submitted outside your application, or your registry lost a recordIdentify its owner before acting on it
On both, with different statusYour record is staleUpdate it from the server's status

The function below performs the comparison, using only the IDs and statuses:

def reconcile(local: dict[str, str], server_batches) -> dict:
    """local maps batch_id -> last status we recorded; server_batches is the listing."""
    server = {batch.id: batch.processing_status for batch in server_batches}
    return {
        "missing_on_server": sorted(set(local) - set(server)),
        "unknown_locally": sorted(set(server) - set(local)),
        "status_changed": sorted(bid for bid in set(local) & set(server) if local[bid] != server[bid]),
    }

Running this on a schedule keeps your registry honest. A batch that appears in status_changed has moved on since you last looked, and the update tells your application to act.


Retention, Expiry, and Deletion

Three time windows apply to every batch, and the list shows each one:

WindowLengthEffect
ProcessingUp to 24 hoursUnfinished requests expire after this (expires_at)
Results download29 days after created_atResults can be downloaded within this window
VisibilityUntil deletedThe batch object remains listed, but results may be gone

A batch in the list after 29 days is a record of what happened, not a source of results. If you need the outputs, download them within the window (Lesson 136).

Deleting a batch is a separate action. The documentation describes a delete endpoint, and notes that a batch still in progress must be canceled first. Deletion removes the batch and its stored inputs and outputs, so delete only when you no longer need them. Your own copy of the results should exist before any deletion.

def delete_finished(client, batch_ids: list[str]) -> int:
    deleted = 0
    for batch_id in batch_ids:
        batch = client.messages.batches.retrieve(batch_id)
        if batch.processing_status == "ended":
            client.messages.batches.delete(batch_id)
            deleted += 1
    return deleted

The check on processing_status matters. Deleting an in-progress batch would fail, so the function skips anything that has not ended.


Practical Example: A Workspace Report

This example pulls the most recent batches, summarizes them, and reconciles them against a local record. The demonstration uses a fake listing, so it runs offline and shows exactly what the report contains.

"""batch_listing.py: list batches, summarize them, and reconcile with local records."""

from collections import Counter
from types import SimpleNamespace


def fake_batch(batch_id, status, succeeded=0, errored=0):
    return SimpleNamespace(
        id=batch_id,
        processing_status=status,
        created_at="2026-11-09T09:00:00Z",
        request_counts=SimpleNamespace(processing=0, succeeded=succeeded, errored=errored,
                                       canceled=0, expired=0),
    )


def latest_batches(listing, count: int = 10) -> list:
    found = []
    for batch in listing:
        found.append(batch)
        if len(found) == count:
            break
    return found


def summarize(batches) -> dict:
    by_status = Counter(batch.processing_status for batch in batches)
    totals = Counter()
    for batch in batches:
        counts = batch.request_counts
        totals.update({"succeeded": counts.succeeded, "errored": counts.errored})
    return {"batches": len(batches), "by_status": dict(by_status), "requests": dict(totals)}


def reconcile(local: dict[str, str], server_batches) -> dict:
    server = {batch.id: batch.processing_status for batch in server_batches}
    return {
        "missing_on_server": sorted(set(local) - set(server)),
        "unknown_locally": sorted(set(server) - set(local)),
        "status_changed": sorted(bid for bid in set(local) & set(server) if local[bid] != server[bid]),
    }


if __name__ == "__main__":
    listing = [
        fake_batch("msgbatch_c", "in_progress"),
        fake_batch("msgbatch_b", "ended", succeeded=480, errored=20),
        fake_batch("msgbatch_a", "ended", succeeded=1000),
    ]
    recent = latest_batches(listing, count=2)
    print("recent:", [b.id for b in recent])
    print("summary:", summarize(listing))

    local_records = {"msgbatch_a": "in_progress", "msgbatch_b": "ended", "msgbatch_old": "ended"}
    print("reconcile:", reconcile(local_records, listing))

Output:

recent: ['msgbatch_c', 'msgbatch_b']
summary: {'batches': 3, 'by_status': {'in_progress': 1, 'ended': 2}, 'requests': {'succeeded': 1480, 'errored': 20}}
reconcile: {'missing_on_server': ['msgbatch_old'], 'unknown_locally': ['msgbatch_c'], 'status_changed': ['msgbatch_a']}

How It Works

  1. latest_batches() stops after the requested count. Against a real listing, that means the iterator fetches only the pages needed for those items.
  2. summarize() reads the counts from each batch object, so the totals come from the list itself, with no extra retrieve calls.
  3. reconcile() compares three sets. msgbatch_old exists locally but not on the server, which signals that it has aged out of the visible window. msgbatch_c exists only on the server, which means some other process submitted it. msgbatch_a has moved from in_progress to ended since the local record was written.
  4. The summary's totals show 1,480 succeeded and 20 errored requests across the visible batches, which is the kind of number a team tracks over time.

Choosing How Often to List

A listing is a single request per page, so it is cheap. The right frequency depends on what you do with it. Reconciliation after each deployment catches drift quickly. An hourly summary is enough for dashboards. Continuous listing is rarely useful, because individual batches are better followed with the status-checking approach from Lesson 134, which watches only the batches you care about.

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 List Message Batches and get answers drawn from it.

Signed-in readers only.