Get Batch Results

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

From a Finished Batch to Usable Data

When a batch reaches ended, its work is done, but its answers are still in a file on the server. Getting them out is the step where many batch projects go wrong, because the file can be large, its lines arrive in no particular order, and each line can describe either a success or one of three kinds of failure. This lesson explains the structure of the results, shows how to stream them without loading everything into memory, and builds a download routine that keeps a verifiable local copy.


What a Result Looks Like

The documentation describes results as a .jsonl file: JSON Lines, where each line is one complete JSON object describing the outcome of one request. A single line for a successful request looks like this:

{"custom_id":"review-8841","result":{"type":"succeeded","message":{"id":"msg_01...","type":"message","role":"assistant","model":"claude-opus-5-5","content":[{"type":"text","text":"positive"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":41,"output_tokens":3}}}}

Each line has two parts. The custom_id identifies the request, exactly as you submitted it (Lesson 133). The result describes the outcome, and its type is one of four values:

Result typeMeaningCarriesBilled
succeededThe request was processed, and produced a messageA full message object, with content, stop_reason, and usageYes
erroredThe request failed, and no message was createdAn error objectNo
canceledThe batch was canceled before this request reached the modelNothing beyond the typeNo
expiredThe 24-hour limit passed before this request reached the modelNothing beyond the typeNo

The first three fields of each line are the same structure you would see in a synchronous response, which means the parsing you already know (Lessons 41 and 42) applies to the message inside a succeeded result.


Order Is Not Guaranteed

The documentation is explicit: results may not come back in the order you submitted the requests. In the example from the documentation, the second request's result appears before the first's. The consequence is that your code must never assume position. Every line must be matched by its custom_id.

This applies even within a single batch that was processed quickly. A design that reads results "in order" and assigns them to a list by index will eventually attach the wrong answer to the wrong record, and the error will be silent. Matching by identifier removes the risk entirely.


Streaming, Not Downloading Everything

The documentation recommends streaming results rather than downloading them all at once, because a large batch can produce a large file. The SDK's results() method yields one result at a time, so memory use stays flat no matter how big the batch is:

for result in client.messages.batches.results(batch_id):
    print(result.custom_id, result.result.type)

Each result is an object with custom_id and result fields, matching the JSON lines. Processing inside the loop means a batch of a hundred thousand results never needs to exist in memory at once.


Saving a Local Copy

Streaming is the right way to read results, but it leaves nothing behind once the loop ends. Results are available for download for 29 days, and after that they are gone. A durable local copy protects against that deadline, against accidental deletion of the batch, and against bugs in your parsing code, since you can always reparse the raw file.

The safest approach writes each result to a file exactly as received, one line per result, and computes a count as it goes. The count can then be compared with the batch's own request_counts, which catches downloads that stopped early:

import json


def save_results(client, batch_id: str, path: str) -> int:
    written = 0
    with open(path, "w", encoding="utf-8") as handle:
        for result in client.messages.batches.results(batch_id):
            handle.write(json.dumps(result.model_dump(), ensure_ascii=False) + "\n")
            written += 1
    return written

model_dump() converts the SDK object back into a plain dictionary, so the file contains the same structure as the documented JSON lines. Writing one JSON object per line keeps the file streamable in the same way the API produced it.


Verifying a Download

A download can end early because of a network interruption, and the loop may finish without raising an error. The batch's own counts provide the check. The total of succeeded, errored, canceled, and expired must equal the number of requests, and the number of lines saved must match that total:

def verify_download(batch, saved_lines: int) -> list[str]:
    counts = batch.request_counts
    expected = counts.succeeded + counts.errored + counts.canceled + counts.expired
    problems = []
    if saved_lines != expected:
        problems.append(f"saved {saved_lines} results, but the batch reports {expected}")
    return problems

If the check fails, rerunning the download is safe, because the batch's results do not change once it has ended. Retrieving them again produces the same lines.


Indexing Results by Custom ID

Once the raw file is saved, a second pass can build an index from custom_id to the outcome. Because the index is a dictionary, lookups are direct, and the order of lines in the file no longer matters:

def index_results(path: str) -> dict[str, dict]:
    index = {}
    with open(path, encoding="utf-8") as handle:
        for line in handle:
            record = json.loads(line)
            index[record["custom_id"]] = record["result"]
    return index

Two requests with the same custom_id would overwrite each other in this index. Lesson 133's registry prevents that by making identifiers unique before submission, and the verification step confirms the count.


Practical Example: Download, Verify, and Index

This example puts the pieces together. It fetches the batch, streams its results to a file, verifies the count, and builds an index. The demonstration uses a fake client and fake results, so it runs offline, and it includes one deliberately short download to show the verification failing.

"""batch_results.py: stream results to a file, verify the count, and index by custom_id."""

import json
from types import SimpleNamespace


def fake_result(custom_id, kind, text=None):
    if kind == "succeeded":
        outcome = {"type": "succeeded", "message": {"content": [{"type": "text", "text": text}],
                                                    "stop_reason": "end_turn"}}
    else:
        outcome = {"type": kind}
    return SimpleNamespace(custom_id=custom_id, model_dump=lambda o=outcome, c=custom_id:
                           {"custom_id": c, "result": o})


class FakeBatches:
    def __init__(self, results):
        self._results = results

    def results(self, batch_id):
        yield from self._results


RESULTS = [
    fake_result("review-8842", "succeeded", "negative"),     # Results can arrive out of order
    fake_result("review-8841", "succeeded", "positive"),
    fake_result("review-8843", "errored"),
]
client = SimpleNamespace(messages=SimpleNamespace(batches=FakeBatches(RESULTS)))
batch = SimpleNamespace(id="msgbatch_demo", request_counts=SimpleNamespace(
    processing=0, succeeded=2, errored=1, canceled=0, expired=0))


def save_results(client, batch_id: str, path: str) -> int:
    written = 0
    with open(path, "w", encoding="utf-8") as handle:
        for result in client.messages.batches.results(batch_id):
            handle.write(json.dumps(result.model_dump(), ensure_ascii=False) + "\n")
            written += 1
    return written


def verify_download(batch, saved_lines: int) -> list[str]:
    counts = batch.request_counts
    expected = counts.succeeded + counts.errored + counts.canceled + counts.expired
    if saved_lines != expected:
        return [f"saved {saved_lines} results, but the batch reports {expected}"]
    return []


def index_results(path: str) -> dict[str, dict]:
    index = {}
    with open(path, encoding="utf-8") as handle:
        for line in handle:
            record = json.loads(line)
            index[record["custom_id"]] = record["result"]
    return index


if __name__ == "__main__":
    written = save_results(client, batch.id, "results.jsonl")
    print("saved lines:", written, "problems:", verify_download(batch, written) or "none")

    index = index_results("results.jsonl")
    for custom_id in sorted(index):
        outcome = index[custom_id]
        text = outcome["message"]["content"][0]["text"] if outcome["type"] == "succeeded" else "-"
        print(f"{custom_id}: {outcome['type']:<10} {text}")

    # A download that stopped early is caught by the count check
    short = SimpleNamespace(id="msgbatch_demo", request_counts=batch.request_counts)
    print("early stop:", verify_download(short, 2))

Output:

saved lines: 3 problems: none
review-8841: succeeded  positive
review-8842: succeeded  negative
review-8843: errored    -
early stop: ['saved 2 results, but the batch reports 3']

How It Works

  1. The results are streamed. save_results() iterates the generator, so each result is written and then released.
  2. Each line keeps the documented shape, because model_dump() returns the same structure the API sends.
  3. The verification compares the saved count with the batch's own tally. The demonstration's last line uses a count of 2 against a total of 3, and the check reports the difference.
  4. The index matches results by identifier. The printed output is sorted by custom_id, even though the file was written in a different order. The sorted output is only for display; the index itself does not depend on order.
  5. Errored results are kept and visible. The review-8843 result appears in the index with its type, so failures are recorded rather than silently dropped.

Handling Each Result Type

The index is a starting point, and the next lesson interprets each outcome in detail. The short version: a succeeded result carries a message to parse, an errored result carries an error that tells you whether to fix the request or retry it, and canceled and expired results mean the request never ran. Keeping all four types in the saved file, rather than filtering them out at download time, lets every later step make its own decision.


Retention Window

Results are available for 29 days after the batch was created. A batch whose results you have not downloaded within that period is still listed (Lesson 135), but the results file is no longer available. The safe pattern is to download as soon as a batch ends, and to treat the saved local file as the permanent record. A scheduled job that checks for ended batches and downloads any without a local copy keeps this automatic.

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 Get Batch Results and get answers drawn from it.

Signed-in readers only.