List Message Batches
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:
| Situation | What it means | What to do |
|---|---|---|
| In your registry, not on the server | The batch was deleted, or it fell outside the visible window | Investigate, and mark the local record accordingly |
| On the server, not in your registry | A batch was submitted outside your application, or your registry lost a record | Identify its owner before acting on it |
| On both, with different status | Your record is stale | Update 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:
| Window | Length | Effect |
|---|---|---|
| Processing | Up to 24 hours | Unfinished requests expire after this (expires_at) |
| Results download | 29 days after created_at | Results can be downloaded within this window |
| Visibility | Until deleted | The 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
latest_batches()stops after the requested count. Against a real listing, that means the iterator fetches only the pages needed for those items.summarize()reads the counts from each batch object, so the totals come from the list itself, with no extra retrieve calls.reconcile()compares three sets.msgbatch_oldexists locally but not on the server, which signals that it has aged out of the visible window.msgbatch_cexists only on the server, which means some other process submitted it.msgbatch_ahas moved fromin_progresstoendedsince the local record was written.- 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.