The Batch API for Bulk Work

Ma Mahalakshmi V Updated 16 Sep 2026
7 min read ·Lesson 58 of 224

When a Request Doesn't Need an Immediate Answer

Every example so far in this course has assumed a request needs its response right away — a user is waiting, an agent's next step depends on the current one finishing, a web request holds a connection open until a reply arrives. A meaningful category of real work doesn't share that constraint at all: classifying ten thousand support tickets overnight, generating embeddings (Unit 10) for an entire document archive, summarizing a week's worth of accumulated logs before a morning report — none of these need a response within seconds, and all of them involve far more individual requests than would be practical to send one at a time through the synchronous APIs used throughout this course. The Batch API is built specifically for this shape of work: large volume, no immediate latency requirement, submitted together and collected later.

What Batch Processing Trades For What

Submitting work as a batch means accepting a longer, less predictable turnaround time — typically up to 24 hours — in exchange for two things a synchronous request doesn't offer: a meaningfully lower per-token cost, and freedom from needing to manage rate limiting (Lesson 3) across thousands of individual synchronous calls yourself.

Note: The exact batch completion window, the specific cost discount compared to synchronous requests, and which endpoints support batch submission can all change over time. Confirm current batch pricing and turnaround guarantees against the current official documentation before relying on a specific number for capacity planning.

This trade only makes sense when the underlying work genuinely doesn't need an immediate answer — the batch API is not a way to make an interactive chat application or one of Unit 11's agents faster or cheaper; it specifically fits work that was already going to run as a background job.

Building a Batch Input File

A batch request is submitted as a file containing one JSON object per line (JSONL), where each line specifies its own custom_id, the endpoint to call, and the request body — structurally similar to a loop of individual client.responses.create() calls, just described up front instead of executed one at a time.

import json

support_tickets = [
    {"id": "ticket-001", "text": "The app crashes every time I try to log in."},
    {"id": "ticket-002", "text": "I was charged twice for my subscription this month."},
    {"id": "ticket-003", "text": "How do I export my data to CSV?"},
]

def build_batch_input_file(tickets: list, output_path: str) -> None:
    with open(output_path, "w") as f:
        for ticket in tickets:
            request_line = {
                "custom_id": ticket["id"],
                "method": "POST",
                "url": "/v1/responses",
                "body": {
                    "model": "gpt-5.6-terra",
                    "input": f"Classify this support ticket into one category "
                             f"(billing, technical, general): {ticket['text']}",
                },
            }
            f.write(json.dumps(request_line) + "\n")

build_batch_input_file(support_tickets, "batch_input.jsonl")

custom_id is what makes it possible to match each result back to the ticket that produced it once the batch completes — since batch results can arrive in a different order than they were submitted, or with some entries failing while others succeed, relying on list position to match a result to its original input is unreliable in a way that relying on custom_id is not.

Submitting the Batch

Submitting a batch is a two-step process: first uploading the JSONL file, then creating a batch job that references the uploaded file.

from openai import OpenAI

client = OpenAI()

uploaded_file = client.files.create(
    file=open("batch_input.jsonl", "rb"),
    purpose="batch",
)

batch_job = client.batches.create(
    input_file_id=uploaded_file.id,
    endpoint="/v1/responses",
    completion_window="24h",
)

print(f"Batch job submitted: {batch_job.id}, status: {batch_job.status}")

Note: The exact method names, required parameters (such as the purpose value and available completion_window options), and the batch job lifecycle's status values can vary by SDK version. Confirm the current batch submission interface against your installed SDK version's documentation.

The endpoint parameter tells the platform which API every line in the input file is targeting — every request within a single batch file needs to target the same endpoint, so a batch mixing embedding requests (Unit 10) and response requests would need to be split into two separate batch files rather than combined into one.

Polling for Completion

A batch job doesn't complete immediately — checking its status periodically is how an application finds out when results are ready to retrieve.

import time

def wait_for_batch_completion(client, batch_id: str, poll_interval_seconds: int = 60):
    while True:
        batch_job = client.batches.retrieve(batch_id)
        if batch_job.status in ("completed", "failed", "expired", "cancelled"):
            return batch_job
        print(f"Batch status: {batch_job.status}, checking again in {poll_interval_seconds}s")
        time.sleep(poll_interval_seconds)

completed_batch = wait_for_batch_completion(client, batch_job.id)
print(f"Final status: {completed_batch.status}")

Polling at a reasonable interval — once a minute is typical for a job with an hours-long completion window — rather than in a tight loop avoids wasting requests checking a status that's unlikely to have changed in the last second; for a batch expected to take hours, a much longer poll interval (or a scheduled check rather than a blocking wait at all) is usually more appropriate than the tight polling loops this course has used elsewhere for fast, synchronous operations.

Retrieving and Matching Results

Once a batch completes, its results are available as an output file (and, separately, an error file for any individual requests that failed), matched back to the original inputs through custom_id.

def process_batch_results(client, completed_batch) -> dict:
    output_file_content = client.files.content(completed_batch.output_file_id)
    results_by_ticket_id = {}

    for line in output_file_content.text.strip().split("\n"):
        result = json.loads(line)
        ticket_id = result["custom_id"]
        response_body = result["response"]["body"]
        results_by_ticket_id[ticket_id] = response_body["output"][0]["content"][0]["text"]

    return results_by_ticket_id

results = process_batch_results(client, completed_batch)
for ticket_id, classification in results.items():
    print(f"{ticket_id}: {classification}")

Note: The exact shape of each result line (including where the actual model output is nested within response.body) can vary by SDK version and by which endpoint the batch targeted. Confirm the current output file format against your installed SDK version's documentation before parsing it in production code.

Building the results_by_ticket_id dictionary keyed on custom_id — rather than assuming results appear in the same order the original tickets were submitted — is what makes this matching reliable regardless of how the platform orders or groups the completed results internally.

Handling Partial Failures Within a Batch

Not every individual request within a batch necessarily succeeds — a batch can complete overall while some of its individual line items failed, and these show up in a separate error file rather than silently disappearing.

def check_for_batch_errors(client, completed_batch) -> list:
    if not completed_batch.error_file_id:
        return []

    error_file_content = client.files.content(completed_batch.error_file_id)
    failed_ticket_ids = []
    for line in error_file_content.text.strip().split("\n"):
        error_entry = json.loads(line)
        failed_ticket_ids.append(error_entry["custom_id"])
    return failed_ticket_ids

failed_ids = check_for_batch_errors(client, completed_batch)
if failed_ids:
    print(f"{len(failed_ids)} requests failed and may need to be resubmitted: {failed_ids}")

Checking for an error file explicitly, rather than assuming a completed batch status means every individual request within it succeeded, is what catches the case where 9,997 of 10,000 tickets classified successfully and three failed for reasons (a malformed input, an individual request exceeding a token limit) worth investigating and potentially resubmitting on their own.

Common Mistakes

Using the Batch API for work that actually needs an immediate response, applying a mechanism built for non-time-sensitive bulk work to an interactive use case it was never designed for.

Relying on result ordering instead of custom_id to match results back to inputs, producing silently mismatched results if the platform returns entries in a different order than submitted.

Assuming a completed batch status means every individual request succeeded, missing partial failures that only appear in a separate error file.

Polling for batch completion in a tight loop, wasting requests checking a status that, for a job with a multi-hour completion window, is very unlikely to have changed within the last few seconds.

Best Practices

Reserve the Batch API for genuinely non-time-sensitive, high-volume work, where the longer turnaround time and lower cost are an acceptable and beneficial trade.

Assign a meaningful, unique custom_id to every request in a batch, and use it — not list position — to match results back to their original inputs.

Always check for and handle a batch's error file, rather than assuming a successfully completed batch job means every individual request within it succeeded.

Poll for batch completion at an interval proportional to the expected completion window, rather than checking in a tight loop suited to fast, synchronous operations.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about The Batch API for Bulk Work and get answers drawn from it.

Signed-in readers only.