Background Workers for Long-Running AI Tasks

Ma Mahalakshmi V Updated 19 Sep 2026
8 min read ·Lesson 195 of 224

Why Some Work Cannot Happen Inside a Request

A web request has an implicit expectation: the client is waiting, and it will time out after some number of seconds (often 30, sometimes less behind a load balancer or API gateway). Many OpenAI SDK workloads do not fit inside that window — summarizing a hundred-page document, running a multi-step agent loop with several tool calls, transcribing a long audio file and then analyzing the transcript, or generating a large batch of images. If work like this runs directly inside an HTTP request handler, either the client's connection times out while the work is still in progress, or you are forced to hold a connection open far longer than is healthy for a web server designed to handle many short-lived requests concurrently.

A background worker is a separate process (or pool of processes) whose only job is to execute tasks handed to it, independent of any web request's lifetime. The web server's job becomes: accept the request, record that the work needs to happen, and immediately respond — typically with a job identifier the client can use to check on progress later. The actual OpenAI API calls happen in the worker process, on its own schedule, unconstrained by any request timeout.

This lesson is about the worker itself — what it is, why it is architected as a separate process, and how to build the simplest version of one. Lesson 7 builds on this by covering the queue that sits between the request handler and the worker in more depth: delivery guarantees, retries, and job status tracking as a first-class concern. It is worth distinguishing this material from Unit 21's batch-processing pipelines as well: Unit 21 covered patterns for processing many similar items together efficiently, such as OpenAI's dedicated Batch API for large offline jobs. This lesson is about general-purpose worker infrastructure for arbitrary long-running tasks submitted one at a time by user-facing requests — the underlying execution model, not the batch-specific cost and scheduling optimizations Unit 21 focused on.

The Simplest Possible Worker

Before reaching for a production task queue library, it helps to understand the core idea with the smallest implementation that demonstrates it: a loop that continuously checks for pending work and executes it.

import time
import uuid
import threading
from dataclasses import dataclass, field
from enum import Enum


class JobStatus(str, Enum):
    PENDING = "pending"
    RUNNING = "running"
    DONE = "done"
    FAILED = "failed"


@dataclass
class Job:
    id: str
    prompt: str
    status: JobStatus = JobStatus.PENDING
    result: str | None = None
    error: str | None = None


class JobStore:
    """In-memory job store, used here for teaching. Production systems use
    a durable store (a database or a queue's own storage) so jobs survive
    a process restart — see Lesson 7."""

    def __init__(self) -> None:
        self._jobs: dict[str, Job] = {}
        self._lock = threading.Lock()

    def create(self, prompt: str) -> Job:
        job = Job(id=str(uuid.uuid4()), prompt=prompt)
        with self._lock:
            self._jobs[job.id] = job
        return job

    def get(self, job_id: str) -> Job | None:
        with self._lock:
            return self._jobs.get(job_id)

    def next_pending(self) -> Job | None:
        with self._lock:
            for job in self._jobs.values():
                if job.status == JobStatus.PENDING:
                    job.status = JobStatus.RUNNING
                    return job
        return None

JobStore tracks jobs by an id and their current JobStatus. The _lock (a threading.Lock) matters because both the web server thread (creating jobs) and the worker thread (claiming and updating jobs) touch the same dictionary concurrently — without the lock, two threads could interleave their reads and writes and either lose a job or claim the same job twice. next_pending atomically finds a pending job and marks it RUNNING in the same locked section specifically to prevent two worker threads from both picking up the same job — a fresh pending job must be claimed exactly once.

The Worker Loop

def run_worker(client, store: JobStore, model: str, poll_interval: float = 1.0) -> None:
    """Continuously poll for pending jobs and execute them.
    In production this runs as its own process, separate from the web server."""
    while True:
        job = store.next_pending()
        if job is None:
            time.sleep(poll_interval)
            continue

        try:
            response = client.responses.create(model=model, input=job.prompt)
            job.result = response.output_text
            job.status = JobStatus.DONE
        except Exception as exc:
            job.error = str(exc)
            job.status = JobStatus.FAILED

This loop is intentionally simple, and every part of it maps to a real production concern. store.next_pending() returning None when there is no work means the worker sleeps for poll_interval seconds rather than spinning in a tight loop burning CPU for no reason — this is polling, the simplest way a worker can discover new work, at the cost of up to poll_interval seconds of latency between a job being created and a worker noticing it. Lesson 7 discusses queue systems that can push work to a worker instead of requiring it to poll, trading some implementation complexity for lower latency and less wasted CPU.

The try/except around the actual client.responses.create call is not incidental — it is the reason a worker architecture is more resilient than inline request handling in the first place. If this call raises (a timeout, a rate limit, a malformed prompt), the exception is caught, the job is marked FAILED with the error recorded, and — critically — the worker loop itself keeps running to process the next job. Without this try/except, one failing job would crash the entire worker process, taking down every other job queued behind it.

Running It End to End

class FakeResponse:
    def __init__(self, text: str) -> None:
        self.output_text = text


class FakeClient:
    class _Responses:
        def create(self, model: str, input: str) -> FakeResponse:
            return FakeResponse(f"Summary of: {input[:20]}")

    @property
    def responses(self) -> "FakeClient._Responses":
        return FakeClient._Responses()


def test_worker_processes_a_pending_job() -> None:
    store = JobStore()
    client = FakeClient()
    job = store.create("A very long document that needs summarizing.")

    # Run one iteration of the worker's core logic directly, rather than
    # starting the infinite loop, so the test terminates deterministically.
    pending = store.next_pending()
    assert pending is not None and pending.id == job.id

    response = client.responses.create(model="gpt-5.6-terra", input=pending.prompt)
    pending.result = response.output_text
    pending.status = JobStatus.DONE

    finished = store.get(job.id)
    assert finished is not None
    assert finished.status == JobStatus.DONE
    assert finished.result is not None and finished.result.startswith("Summary of:")
    print("PASS: worker processes a pending job end to end")


test_worker_processes_a_pending_job()

This test does not call run_worker directly, because run_worker contains an infinite while True loop that would never return. Instead, it exercises the same logic — claim a pending job, call the client, record the result — as a single, deterministic sequence of steps. This is a common and useful pattern when testing loop-based worker code: extract or replicate the loop's body into something testable, rather than trying to run the loop itself under test with an artificial exit condition.

Why the Worker Is a Separate Process

In a real deployment, run_worker does not share a process with your web server. It runs as its own container (built from the same image described in Lesson 3, but started with a different CMD — for example, CMD ["python", "worker.py"] instead of CMD ["uvicorn", ...]) or as a separately deployed service. This separation matters for a few concrete reasons: the web server can be scaled independently of the number of worker processes (Lesson 8 covers horizontal scaling in depth); a worker crash does not take down request handling for users who are not waiting on background jobs; and worker processes can be given different resource limits (more memory, longer allowed runtimes) appropriate to long-running AI tasks, without over-provisioning every web server replica the same way.

Note: Production systems rarely hand-roll a polling loop and an in-memory dict as shown here. Mature task-queue libraries — Celery, RQ, and Arq are common choices in the Python ecosystem — provide durable job storage (typically backed by Redis or a message broker), automatic retries, scheduled and periodic jobs, and worker pools out of the box. The teaching implementation in this lesson exists to make the underlying mechanism explicit before you adopt one of those libraries; the concepts (claim a job, execute it, record the outcome, keep the loop alive across individual failures) are the same either way.

Common Mistakes

Letting an unhandled exception in job processing crash the worker loop. Without a try/except around the actual task execution, one bad input can take down the worker entirely, silently stalling every job queued behind it until someone notices and restarts the process.

Running background work inside the request-handling thread or process instead of a genuinely separate one. This defeats the purpose — a slow job still blocks the web server's ability to handle other requests promptly, especially in a single-threaded or limited-worker-pool web server configuration.

Polling too aggressively. A poll_interval of zero or a few milliseconds turns the worker into a tight loop that consumes CPU and, if it is also checking a shared datastore, adds unnecessary load there for no meaningful latency benefit over a more reasonable interval.

Best Practices

Isolate the failure of one job from every other job with a try/except around task execution, so the worker loop's resilience does not depend on every individual task being bug-free.

Deploy workers as a separately scalable process from the web server, using the same container image but a different startup command, so the two can be resourced and scaled according to their very different workload characteristics.

Start with the simplest mechanism that works, and adopt a mature task-queue library once you need its guarantees — durable storage across restarts, automatic retries, and visibility into queue depth are exactly the concerns Lesson 7 covers next.

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 Background Workers for Long-Running AI Tasks and get answers drawn from it.

Signed-in readers only.