Handling Partial Failures in Bulk Workloads

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

Handling Partial Failures in Bulk Workloads

Unit 12, Lesson 5 covered what the Batch API itself does with failures: a submitted batch job produces both an output file for successful requests and a separate error file listing which requests failed and why, matched back by custom_id. That is failure handling at the API level — it tells you which HTTP-level requests didn't succeed. This lesson is about a different, harder problem: failure handling at the pipeline and business-logic level, where "the API call succeeded" and "this item was processed correctly" are not the same statement, and where a bulk job finishing with 9,850 out of 10,000 items succeeded raises real operational questions that a pass/fail count alone doesn't answer.

Partial Failure Is the Normal Case, Not the Exception

A synchronous, single-request feature can reasonably treat failure as exceptional — one user, one request, and if it fails you show an error and let them retry. A bulk job processing tens of thousands of independent items behaves completely differently: at that scale, some nonzero failure rate is close to guaranteed, whether from transient network issues, occasional malformed source data, content that trips a safety filter, or responses that don't parse the way you expected. Designing a bulk pipeline around the assumption that failures are rare and exceptional produces brittle code that either crashes the whole job on the first failure or silently drops failed items with no record of what happened to them. Designing around the assumption that failures are a normal, expected fraction of any large run produces a pipeline that finishes reliably and hands you a clear, actionable account of what needs attention.

A Failure Taxonomy: Not All Failures Are the Same

Treating every failure identically — log it and move on — throws away information that matters for what to do next. A useful taxonomy separates failures along two axes: where they occurred, and whether retrying is likely to help.

Failure categoryExampleRetry likely to help?
Transient API errorNetwork timeout, temporary 5xxYes
Rate limit429 responseYes, with backoff
Permanent API rejectionContent policy violation, invalid requestNo
Response parsing failureModel output didn't match expected structureSometimes (retry with adjusted prompt)
Source data problemMissing required field, malformed inputNo — needs upstream data fix
Downstream write failureDatabase write after a successful model call failedYes, but must avoid reprocessing the model call

The last row deserves particular attention because it's the case most pipelines get wrong: if the model call succeeded but writing the result to your database failed, retrying "the item" naively means calling the model again, paying for a duplicate request, when the actual problem was entirely on the write side. This is exactly why the record-based pipeline design from Lesson 2 matters — a record's status should be granular enough to distinguish "model call failed" from "model call succeeded but downstream write failed," so the retry logic in Lesson 8 can act on the correct step rather than restarting the whole item from scratch.

from enum import Enum


class FailureStage(str, Enum):
    MODEL_CALL = "model_call"
    RESPONSE_PARSING = "response_parsing"
    DOWNSTREAM_WRITE = "downstream_write"
    SOURCE_DATA = "source_data"


class FailureCategory(str, Enum):
    TRANSIENT = "transient"      # safe and likely useful to retry
    PERMANENT = "permanent"      # retrying will not help

Designing a Per-Item Result Record for Failures

Extending the PipelineRecord from Lesson 2 with explicit failure metadata turns a vague error: Optional[str] field into something the pipeline's finalize stage — and a human reviewing the run afterward — can act on directly:

from dataclasses import dataclass, field
from typing import Optional


@dataclass
class FailureInfo:
    stage: FailureStage
    category: FailureCategory
    message: str
    raw_error: Optional[str] = None


@dataclass
class ItemResult:
    record_id: str
    success: bool
    output: Optional[str] = None
    failure: Optional[FailureInfo] = None


def classify_exception(exc: Exception, stage: FailureStage) -> FailureInfo:
    message = str(exc)
    lowered = message.lower()
    if "rate_limit" in lowered or "timeout" in lowered or "connection" in lowered:
        category = FailureCategory.TRANSIENT
    elif "invalid_request" in lowered or "content_policy" in lowered:
        category = FailureCategory.PERMANENT
    else:
        category = FailureCategory.TRANSIENT  # default to retryable unless proven otherwise
    return FailureInfo(stage=stage, category=category, message=message, raw_error=message)

classify_exception makes an explicit, reviewable decision about whether a given error is worth retrying, rather than leaving that judgment to be re-derived ad hoc every time someone looks at a failure log. Defaulting unknown errors to TRANSIENT is a deliberate, conservative choice: it's usually safer to retry an item that turns out not to need it (a wasted, cheap re-attempt) than to permanently give up on an item that would have succeeded on a second try.

Processing an Item Stage-by-Stage, Catching Failures at Each One

Wiring the taxonomy into actual item processing means wrapping each stage — model call, parsing, downstream write — in its own error handling, so a failure's stage field is always accurate:

async def process_item_with_failure_tracking(client, record) -> ItemResult:
    # Stage 1: model call
    try:
        response = await client.responses.create(
            model="gpt-5.6-terra",
            input=record.prompt,
        )
    except Exception as exc:
        return ItemResult(
            record_id=record.record_id,
            success=False,
            failure=classify_exception(exc, FailureStage.MODEL_CALL),
        )

    # Stage 2: parse the response into the expected structure
    try:
        parsed_output = parse_model_output(response.output_text)
    except Exception as exc:
        return ItemResult(
            record_id=record.record_id,
            success=False,
            failure=classify_exception(exc, FailureStage.RESPONSE_PARSING),
        )

    # Stage 3: write the result downstream
    try:
        await write_result_downstream(record.record_id, parsed_output)
    except Exception as exc:
        return ItemResult(
            record_id=record.record_id,
            success=False,
            output=parsed_output,  # the model result exists even though the write failed
            failure=classify_exception(exc, FailureStage.DOWNSTREAM_WRITE),
        )

    return ItemResult(record_id=record.record_id, success=True, output=parsed_output)


def parse_model_output(text: str) -> str:
    if not text or not text.strip():
        raise ValueError("empty model output")
    return text.strip()


async def write_result_downstream(record_id: str, output: str) -> None:
    # Placeholder for a real database or storage write.
    pass

The key detail is in the DOWNSTREAM_WRITE failure branch: output=parsed_output is preserved on the ItemResult even though the overall item is marked as failed. This is exactly the information that prevents a wasteful, incorrect retry later — Lesson 8 shows how a retry step can check for this preserved output and skip straight to re-attempting the write, instead of calling the model again for an item whose model output was already obtained successfully.

Quarantining Non-Retryable Failures

Permanent failures — a content policy rejection, a source record missing a required field — should not sit in the same retry queue as transient ones. Routing them to a separate destination (a "needs human review" table or file) makes the distinction operationally real rather than just a label on a log line:

def route_failed_results(results: list[ItemResult]) -> dict[str, list[ItemResult]]:
    retry_queue = []
    quarantine = []

    for result in results:
        if result.success:
            continue
        if result.failure.category == FailureCategory.TRANSIENT:
            retry_queue.append(result)
        else:
            quarantine.append(result)

    return {"retry": retry_queue, "quarantine": quarantine}

Separating these two lists means the pipeline's next run only attempts the items that have a real chance of succeeding, and a human reviewing the quarantine list is looking exclusively at items that genuinely need a data fix or a manual decision — not wading through a mix of both to find the ones worth their attention.

Testing Failure Classification and Routing

Because classify_exception and route_failed_results are pure functions with no I/O, they test cleanly with fake exceptions and fake results:

def test_classify_exception_identifies_transient_errors():
    info = classify_exception(TimeoutError("request timeout"), FailureStage.MODEL_CALL)
    assert info.category == FailureCategory.TRANSIENT
    print("PASS: timeout classified as transient")


def test_route_failed_results_separates_retry_and_quarantine():
    results = [
        ItemResult(record_id="1", success=True, output="ok"),
        ItemResult(
            record_id="2", success=False,
            failure=FailureInfo(FailureStage.MODEL_CALL, FailureCategory.TRANSIENT, "timeout"),
        ),
        ItemResult(
            record_id="3", success=False,
            failure=FailureInfo(FailureStage.MODEL_CALL, FailureCategory.PERMANENT, "policy"),
        ),
    ]
    routed = route_failed_results(results)
    assert len(routed["retry"]) == 1
    assert len(routed["quarantine"]) == 1
    assert routed["retry"][0].record_id == "2"
    assert routed["quarantine"][0].record_id == "3"
    print("PASS: failures routed to retry vs quarantine correctly")


test_classify_exception_identifies_transient_errors()
test_route_failed_results_separates_retry_and_quarantine()

Common Mistakes

  • Treating every failure as equally worth retrying (or equally worth giving up on). A content-policy rejection will fail identically on every retry; retrying it wastes time and cost. A network timeout is usually fine on a second attempt. Conflating the two means either endless useless retries or giving up on items that would have succeeded.
  • Discarding partial progress on a multi-stage item when only the last stage failed. If the model call succeeded and only the database write failed, redoing the model call on retry is an avoidable cost and, worse, sets up the duplication risk covered in Lesson 8.
  • Reporting failures only as a raw count with no breakdown. "150 items failed" tells an operator nothing actionable; "150 items failed: 12 permanent content-policy rejections needing manual review, 138 transient timeouts queued for retry" tells them exactly what to do next.

Best Practices

  • Classify every failure by stage and by retry-worthiness at the point it occurs, while you still have the original exception, rather than trying to reconstruct that information later from a generic error string.
  • Preserve any partial output already obtained before a later stage fails, so a subsequent retry can resume from the correct step instead of repeating already-successful work.
  • Route permanent and transient failures to separate destinations — an automatic retry queue for the former, a human-reviewable quarantine list for the latter — so the systems and people handling each can each see exactly the failures relevant to them.

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 Handling Partial Failures in Bulk Workloads and get answers drawn from it.

Signed-in readers only.