Skip to main content
December 2025 Update: Production patterns for processing thousands of LLM requests efficiently while respecting rate limits and managing costs.

Why Batch Processing?

Every LLM application eventually hits the moment where you need to process not 10 requests, but 10,000. Maybe you’re embedding an entire document corpus, classifying a backlog of support tickets, or generating product descriptions for a catalog. At this scale, sequential processing is not slow — it’s comically impractical. The math makes this concrete: if each LLM call takes 3 seconds and you have 10,000 prompts, sequential processing takes 8.3 hours. With batched concurrent processing (20 workers), that drops to about 25 minutes. With the OpenAI Batch API, you submit the whole thing and come back in a few hours — at 50% the cost.

OpenAI Batch API

OpenAI’s Batch API is a fundamentally different approach: instead of making real-time API calls, you submit a file of requests and OpenAI processes them asynchronously within a 24-hour window. The tradeoff is latency for cost — you get 50% off both input and output tokens. When to use it: anytime you don’t need results immediately. Data pipelines, nightly report generation, bulk content creation, embedding large corpora, evaluation runs — all perfect candidates. When NOT to use it: anything user-facing or time-sensitive. Think of it like the difference between express and standard shipping: same package, same destination, but standard is half the price because the carrier can optimize their route.
Batch API gotcha: The 24-hour completion window is not a guarantee — it’s a maximum. Most batches complete much faster (minutes to hours), but don’t build workflows that assume completion in under 24 hours. Also, individual requests within a batch can fail even if the batch succeeds. Always check per-request error fields in the results.

Concurrent Processing with Rate Limiting

When you can’t wait for the Batch API (need results in minutes, not hours), the real-time concurrent pattern is your tool. This fires multiple requests simultaneously while respecting rate limits — think of it as a controlled firehose. The three knobs you’re tuning: requests per minute (RPM), tokens per minute (TPM), and max concurrent connections. Start conservative and increase. It’s far better to process a batch in 10 minutes at 80% rate utilization than to get rate-limited into a retry spiral that takes 30 minutes.
Tuning max_concurrent: Start at 10-20 and monitor your 429 error rate. If it is 0%, you have headroom to increase. If it exceeds 1%, reduce concurrency. The optimal value depends on your API tier, average prompt size, and time of day (providers throttle harder during US business hours).

Queue-Based Processing

For very large volumes (100K+ items), asyncio.gather won’t cut it — you can’t create 100K coroutines and hope for the best. The queue-based pattern decouples “submitting work” from “processing work” using a producer-consumer model with a fixed pool of workers. This is the same pattern that powers every serious data pipeline: RabbitMQ consumers, Celery workers, SQS processors. The async queue version is lighter-weight (no external broker needed) but follows the same principles.
Memory trap with large batches: The results dict in JobQueue stores every job object in memory. For 100K+ items with large responses, this can consume several GB. For truly large workloads, write results to disk or a database as they complete rather than accumulating them in memory. A good pattern is to pass a result_callback to the worker that persists each result immediately.

Chunked Processing for Large Datasets

When datasets get really large (10K+ items), processing everything as one giant batch creates problems: no progress visibility, no recovery from mid-batch failures, and potential memory issues from holding all results in memory. Chunking solves this by breaking the dataset into manageable pieces and processing each chunk independently. The inter-chunk delay is a deliberate breather. It gives the API provider time to recover between bursts and prevents your rate limiter from drifting (accumulated rounding errors can cause brief rate limit violations at chunk boundaries).
Choosing chunk_size: Smaller chunks (50-100) give finer progress granularity and faster recovery from crashes but add overhead from inter-chunk delays. Larger chunks (500-1000) are more efficient but mean more lost progress on failure. A good heuristic: set chunk_size so each chunk takes 1-5 minutes to process. This balances efficiency with acceptable data loss on crash.

Progress Tracking and Checkpointing

Checkpointing is the difference between “my script crashed at item 7,500 and I have to start over” and “my script crashed at item 7,500 and resumed from exactly where it left off.” For any batch job that takes more than 10 minutes, checkpointing is not optional — it’s a basic production requirement. The pattern is simple: periodically serialize your progress to disk. On restart, check if a checkpoint exists and resume from the last saved position. The tricky part is making this atomic — a crash during checkpoint writing shouldn’t corrupt your state.
Checkpoint file size: The results list in ProcessingState stores all results in the checkpoint file. For 100K items with 500-character responses each, that is a ~50MB JSON file. For larger workloads, store results in a separate file or database and only keep the index and metadata in the checkpoint. This also makes checkpoint writes faster, reducing the crash-during-write risk window.

Cost Estimation

Never run a large batch without estimating cost first. This sounds obvious, but the number of teams that have accidentally burned 5,000onabatchjobtheyexpectedtocost5,000 on a batch job they expected to cost 50 is staggering. The most common mistake: forgetting that output tokens cost 2-5x more than input tokens, and underestimating average output length. Always run a small sample (50-100 items) first to measure actual token usage, then extrapolate. The estimate below uses character-count heuristics, which are accurate to within ~20% for English text.
The real cost trap is output tokens. Input pricing gets all the attention, but output tokens cost 2-7x more depending on the model. A batch job where you expect 100-token responses but the model averages 800 tokens (because your prompt does not constrain output length) can cost 8x your estimate. Always run a 50-100 item sample first and measure actual output lengths before committing to a full run.

Key Takeaways

Use Batch API

OpenAI Batch API saves 50% on costs for async workloads

Rate Limit Properly

Token bucket algorithms prevent hitting API limits

Checkpoint Progress

Save progress for recovery from failures

Estimate Costs

Always estimate before running large batches

Interview Deep-Dive

What interviewers are testing: Resilience engineering and operational maturity for long-running data pipelines.Strong answer: This is a checkpointing and idempotency problem. The system needs three things: progress persistence, safe resumption, and deduplication.First, checkpoint every N items (100-500 is a good range). Each checkpoint writes the last successfully processed index and all results so far to a durable store (local file, S3, or database). The checkpoint write itself should be atomic — write to a temp file, then rename — so a crash during checkpointing doesn’t corrupt state.Second, on restart, load the checkpoint and resume from last_processed_index + 1. The items before that index are already done, so skip them. The items after that index need processing. Items in the “gap” (between last checkpoint and the crash point) might or might not have been processed — so you need idempotency.Third, for idempotency: use deterministic IDs for each request (e.g., hash of the prompt + index). Before processing an item, check if a result already exists for that ID. This makes retries safe — processing the same item twice produces the same result entry, not a duplicate.The provider outage itself should trigger a circuit breaker: after 5-10 consecutive failures, pause processing and wait with exponential backoff before probing again. Don’t burn through your retry budget on 33,000 remaining items when the provider is down.Cost protection: track cumulative spend during the batch. If it exceeds a configurable threshold (say, 150% of estimated cost), halt and alert. Provider outages can sometimes cause partial responses that still incur charges.
What interviewers are testing: Understanding of cost-latency tradeoffs and operational complexity in real production systems.Strong answer: The decision comes down to four factors: latency requirements, cost sensitivity, operational complexity tolerance, and control needs.OpenAI Batch API: 50% cost savings, up to 24-hour completion window, zero infrastructure to manage, no rate limit concerns (OpenAI manages scheduling internally). Choose this for: nightly data pipelines, evaluation runs, content generation backlogs, embedding large corpora — anything where “done by tomorrow morning” is fast enough.Self-managed concurrent processing: Results in minutes instead of hours, full control over retry logic and error handling, ability to use multiple providers with fallback, real-time progress visibility. Choose this for: time-sensitive batch jobs (process uploaded files within 30 minutes), workflows that need partial results quickly, multi-provider strategies, or when you need custom logic like filtering mid-batch based on intermediate results.The hybrid approach often wins: use self-managed concurrency for the urgent 20% of items (new uploads, high-priority customers) and route the remaining 80% through the Batch API for cost savings. This requires a routing layer that classifies items by urgency and dispatches them to the appropriate processing path.One often-overlooked factor: the Batch API doesn’t support streaming, function calling, or vision inputs in all configurations. Verify that your specific use case is supported before committing to it for a production pipeline.
What interviewers are testing: End-to-end thinking about large-scale ML data pipelines, cost optimization, and risk management.Strong answer: At this scale, you’re operating more like a data engineering project than a simple script. The approach has four phases.Phase 1 — Validate and sample: Process a random sample of 500 documents first. Measure actual token usage (not estimates), quality of results, failure rate, and per-item cost. Extrapolate to confirm the 15Kestimate.Ifqualityisborderline,thisiswhereyoutunethepromptbeforeburning15K estimate. If quality is borderline, this is where you tune the prompt before burning 15K on bad results.Phase 2 — Optimize cost: Can you use a cheaper model? If gpt-4o-mini gives 95% of gpt-4o’s classification accuracy, you’ve just cut costs by 80%. Can you shorten prompts? Every token in the system message gets multiplied by 1M. Can you use the Batch API? That’s 7,500insteadof7,500 instead of 15,000. Can you pre-filter obviously classifiable documents with a rules-based system and only send ambiguous ones to the LLM? That might eliminate 40% of calls.Phase 3 — Execute with guardrails: Split into daily batches of ~100K (spreading cost over 10 days reduces blast radius if something goes wrong). Set daily cost limits. Checkpoint every 1,000 items. Run quality spot-checks on each day’s output before proceeding to the next batch. Use the Batch API for the bulk and concurrent processing only for rush items.Phase 4 — Long-term: If this is recurring, train a fine-tuned model or a traditional ML classifier on the LLM-labeled data. A fine-tuned gpt-4o-mini is much cheaper per call, and a distilled BERT classifier running on your own hardware costs essentially nothing at inference time. The $15K LLM run becomes your labeling step, not your production system.The meta-point: at $15K, this needs a project plan, not just a script. Stakeholder buy-in on cost, rollback plans, quality gates, and a path to cost reduction over time.
What interviewers are testing: Practical cost governance for AI systems, which is an increasingly critical production concern.Strong answer: Build a cost tracker as a first-class component, not an afterthought.The CostTracker maintains a running total of actual spend (from API response usage fields, not estimates) and compares against configurable thresholds. It exposes three states: green (under 80% of budget), yellow (80-100%, switch to cheaper model), and red (over budget, pause processing).Implementation: After each API call, extract the actual token counts from the response and calculate cost using the model’s pricing. Update the running total atomically (this tracker is shared across concurrent workers). At each threshold crossing, execute the configured action.Actions at yellow threshold: Automatically downgrade remaining items from gpt-4o to gpt-4o-mini. Log the switchover for quality tracking. Alert the team via Slack/PagerDuty so humans can decide whether to increase the budget or accept the quality tradeoff.Actions at red threshold: Stop submitting new items. Let in-flight requests complete (don’t waste already-spent money). Save checkpoint. Send an alert with a summary: items completed, items remaining, actual cost vs. budget, and estimated cost to complete.The nuance most people miss: token estimation before the call is unreliable for output tokens (you don’t know how much the model will generate). So budget tracking must be based on actual usage, not pre-estimates. This means you might slightly overshoot the budget by the cost of in-flight requests when the red threshold triggers. Account for this buffer in your budget planning.

What’s Next

LLM Fallbacks

Build resilient systems with multi-provider fallback chains