Vibe Engines
YouTube
AI System Design

Design a Batch Inference System

Learn AI system design by building an offline batch LLM inference system that processes millions of prompts step by step.

The numbers to beatjobdataset + templateasyncprogress + job idresultskeyed by input id

The whole design, in writing

Learn AI system design by building an offline batch LLM inference system that processes millions of prompts step by step. An interactive guide covering why looping the online API fails, optimizing throughput over latency, a restartable sharded pipeline, batching techniques (continuous batching, length bucketing, prefix caching), scaling on spot GPUs with checkpointing, cost optimization, and the unhappy paths (failures at scale, idempotency, skew).

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What is batch inference?

Sometimes you don't need an answer now — you need millions of answers, cheaply, by tomorrow: classify every support ticket, summarize a document corpus, embed a whole catalog, or run an eval over a huge test set. This is offline LLM inference, and it has the opposite priorities of a chatbot: nobody's waiting on any single result, so throughput and cost matter, not latency.

Clientmillions of prompts
New in this step: Client.

A batch inference system grinds a huge dataset through models at maximum throughput per dollar. The moves are the inverse of online serving: huge batches to saturate GPUs (latency is free to trade away), a restartable sharded pipeline, and aggressive cost optimization (spot GPUs, batch discounts) — with checkpointing so a job over millions of items survives failures.

What the new pieces do

Clientclient
Submits a big offline job — classify/summarize/embed millions of documents, or run an eval — where throughput matters, not per-item latency.

Step 1 · Not the online API in a loop

Why looping fails

The obvious approach: loop over your dataset and call the online inference API once per item. Why is that a terrible way to process millions of prompts?

Clientmillions of prompts
The system as it stands at this step.

Loop over millions of items calling the online API one at a time. What's wrong?

  1. Simple but disastrous at scale: it under-utilizes GPUs (tiny per-request batches), hits rate limits, pays online (latency-optimized) prices, and offers no restartability for a job that runs for hours over millions of items.

  2. Answers are fine; efficiency is the problem. One-at-a-time calls waste GPU throughput, get rate-limited, cost more, and can't cleanly resume after a failure mid-job.

  3. The online API is tuned for low latency per request, not throughput, so serial calls leave GPUs mostly idle, run into rate limits, cost far more per token, and a crash partway through a multi-hour job means unclear recovery. Batch inference is a different system optimized for throughput and resilience.

Looping the online API is disastrous at scale: it under-utilizes GPUs (one small request at a time), hits rate limits, pays latency-optimized prices (online serving costs more per token), and has no clean restart if a multi-hour job fails partway. Batch inference is a separate system built for throughput and resilience — the opposite optimization target from online serving.

Why this piece earns its place

The repair everyone reaches for first is to fan the loop out — a thread pool, many calls in flight — and it is worth being able to say why that still does not get you there. Concurrency at the client is not batching at the server. Each of those calls arrives on its own and is scheduled on its own; the provider may coalesce them for its own efficiency, but it does that inside a latency target it owes other people, so the one number that decides the economics here — how many prompts sit in a single forward pass — is set by somebody optimizing for the opposite thing. You have bought parallelism at the wrong layer. The loop also has nowhere to keep its progress. That progress is a variable inside a running process, so there is nothing for a second attempt to read, and we will make it restartable later turns out to be a rewrite rather than a patch. There is a real line where the loop is still the right call, though. If starting over is annoying rather than expensive, a script with a resume file beats a job system you now have to operate. The threshold is not item count — it is whether the run outlives the terminal it was started from.

Step 2 · A job, not a request

The batch job model

If it's not per-request, what's the interface? What does a client submit, and what does the system promise?

Clientmillions of promptsBatch APIsubmit job
New in this step: Batch API.

Model it as a job: the client submits a dataset (millions of inputs) + a prompt template + model choice, and the system returns a job id, processes asynchronously, reports progress, and writes outputs to a results store (each keyed by its input id). There's no synchronous per-item response — you poll or get notified when the job (or a portion) completes. This job abstraction is what lets everything behind it optimize for throughput and survive failures.

Why this piece earns its place

Look at what the client can actually hand over. Millions of inputs do not travel in a request body, so the dataset arrives as a reference — a path, a table name, a prefix in object storage — and the job then reads that reference for hours. It is mutable, and nothing in the diagram says otherwise. Rows appended mid-run are processed or skipped depending on where a worker had got to; rows edited in place get whichever version the worker happened to read, and a retried shard can produce the other one; a file replaced at hour three fails shards that were fine at hour one. So the submit path has to resolve and freeze the dataset — an immutable snapshot, or at minimum a version id and an item count the scheduler shards against — because everything downstream assumes the work is a fixed set. Two smaller obligations come with the job model. Cancel is a real terminal state, not a UI affordance: somebody will submit the wrong dataset, and a job whose only ending is finishing will keep buying GPU hours to write output nobody will read, so cancel has to reach the workers. And the job id is the only usable cost-attribution key once several teams share one fleet — what did that experiment cost is unanswerable per instance and answerable per job.

  • jobdataset + template
  • asyncprogress + job id
  • resultskeyed by input id

What the new pieces do

Batch APIgateway
Accepts a dataset + prompt template, kicks off a job, and reports progress — no synchronous per-item response.

Step 3 · Saturate the GPU

Throughput via big & continuous batching

The core efficiency lever: GPUs are massively parallel and idle most of the time on one request. How do you keep them fully fed when you have millions of prompts and no latency pressure?

Clientmillions of promptsBatch APIsubmit job
The system as it stands at this step.

You have millions of prompts and no latency SLA. How do you maximize GPU throughput?

  1. With no per-item latency requirement, you feed the GPU as many prompts in parallel as memory allows (large batches), and use continuous/in-flight batching so finished sequences are immediately replaced by new ones — keeping the GPU near 100% utilization. You're spending latency you don't care about to buy throughput you do.

  2. One-at-a-time leaves the GPU almost idle — the single biggest waste in inference. Batch inference exists precisely to fill the GPU with parallel work.

  3. Hardware choice aside, the throughput win comes from batching work onto each GPU. Without large/continuous batching, even more GPUs stay underutilized.

Run large batches and continuous (in-flight) batching: feed the GPU as many prompts in parallel as memory allows, and as each sequence finishes, immediately slot in another so the GPU never idles. Because you have no latency SLA, you can push batch sizes far higher than online serving would tolerate — spending latency you don't care about to buy the throughput you do. This is the single biggest lever: a saturated GPU processes many times the tokens-per-dollar of a serial loop.

Why this piece earns its place

Two things here you only learn by running it. First, the batch size is a memory budget, not a number. What fills the card is the KV cache, and that grows with sequence length times concurrency, so the largest safe batch depends on what happens to be in it. Cap it by tokens in flight rather than by request count, because setting it too high does not make the worker slow — it gets the process killed partway through a shard, and with continuous batching that kill takes every sequence currently in flight with it, not one. Second, the near-100% utilization this step is chasing is a floor check, not a tuning signal. It tells you the worker is batching at all, which a serial loop never manages, and then it stops moving as a number long before the tuning is done. What the driver reports is that a kernel was resident, not that the hardware did useful work, so a worker can sit pinned there while it shuffles memory or computes padding — which is how a throughput regression survives a week of green dashboards. Once you are tuning rather than checking, the number that answers the question is output tokens per GPU-second against a fixed sample.

  • big batchesfill GPU memory
  • continuousrefill finished slots
  • ~100%GPU utilization

Step 4 · Spread the work

A restartable sharded pipeline

Millions of items won't fit through one worker, and a job running for hours will hit failures. How do you parallelize the work and make it survive crashes?

Batch APIsubmit jobWork QueueshardedSchedulershard · autoscaleInference Workerbig batchesResults Storeoutputs
New in this step: Work Queue, Scheduler, Inference Worker, Results Store.

Build a sharded pipeline: a scheduler splits the dataset into shards placed on a durable queue; many workers pull shards, run batched inference, and write outputs to the results store keyed by input id. Because work is sharded through a durable queue and outputs are keyed idempotently, a lost worker's shard is simply retried by another, and the job is restartable — resume the unfinished shards, don't restart the whole thing. Autoscale the worker fleet to the backlog.

Why this piece earns its place

A worker here is not a task, and that carries further than it looks. Loading the model is the expensive part of starting one — the weights have to be pulled and made resident in GPU memory before a single prompt is processed, and on anything worth batching that dominates startup — so a worker is a long-lived process that takes shard after shard, never a process spawned per shard. That bends the scaling policy away from anything a web tier does. Scale-up is measured in minutes rather than the seconds an HTTP autoscaler assumes, so you scale on the trend of the backlog instead of its depth at this instant, and you keep a worker warm between shards rather than handing it back, because a fleet that scales down eagerly pays that load time again on the way back up. It also changes what a failed shard means. Because one worker takes many shards in a row, a host with a sick GPU, a half-written model directory or a full disk fails everything it touches, and a shard returned to the queue in front of a fast-draining fleet often lands straight back on the machine that just failed it. Count failures per worker as well as per shard, and take a host out of the fleet when it trips — otherwise one bad machine quietly eats the run.

  • shardpartition the dataset
  • workerspull + process in parallel
  • idempotentwrite by input id

What the new pieces do

Work Queuebus
Durably holds the job's work items in shards, so many workers pull in parallel and a lost worker's shard is retried.
Schedulerservice
Splits the dataset into shards, tracks progress, and scales the worker fleet (often on spot GPUs) to the backlog.
Inference Workerservice
Pulls a shard and runs the model over it in LARGE batches to saturate the GPU — throughput over latency.
Results Storedata
Where outputs land, keyed by input id so results are idempotent and the job is resumable.

Step 5 · Squeeze each batch

Bucketing & prefix caching

Even with big batches, throughput leaks: prompts vary wildly in length (padding waste), and many prompts share the same long template prefix. How do you extract more?

Inference Workercontinuous batchingModelweights
New in this step: Model.

Two throughput wins. Length bucketing: group prompts of similar length into a batch so you don't pad short sequences up to the longest one (padding is wasted compute) — sort/bucket by token length. Prefix caching: when many prompts share a common prefix (the same instruction/template, few-shot examples, or a shared document), compute that prefix's KV cache once and reuse it across all of them, so you only pay for the differing part. Both exploit structure batch jobs have but online traffic often lacks.

Why this piece earns its place

Both of these have a consequence that lands somewhere else in the design. Bucketing reorders the dataset: results stop arriving in input order, permanently, and anything downstream that assumed a stable order is now wrong in a way no test on a small file will catch. That is what makes keying every write by input id non-negotiable rather than merely tidy. Prefix caching has a sharper edge. The cache is keyed on the exact token prefix, so one character of drift breaks it — a trailing space, a date rendered into the system prompt, or a per-item field someone placed above the shared instructions instead of below them. Nothing errors and nothing logs; you quietly pay the full prompt for every item in the run. So prompt layout is a performance decision: everything shared goes first, everything per-item goes last, and the template is a file you diff rather than a string you edit in place. Skip bucketing entirely if the prompts are already uniform — a fixed-form classification job gains almost nothing from it, and you have added a sort pass over a dataset that does not fit in memory.

  • bucket by lengthcut padding waste
  • prefix cachecompute shared prompt once
  • reuseacross the batch

What the new pieces do

Modeldata
The loaded model, fed as many prompts in parallel as fit — continuous batching keeps it fully utilized.

Step 6 · Survive hours & spot GPUs

Checkpointing

A ten-million-prompt job runs for hours, and if you run it on cheap spot GPUs (which can be reclaimed anytime), workers will vanish mid-job. How do you not lose (or repay for) work?

Schedulershard · autoscaleInference Workercontinuous batchingModelweightsCheckpointprogress
New in this step: Checkpoint.

A long job on interruptible spot GPUs — workers vanish mid-run. How do you not lose or repeat work?

  1. That works but throws away the biggest cost saving (spot GPUs are far cheaper). With checkpointing, batch jobs tolerate interruptions, so you can use cheap spot capacity.

  2. Restarting from zero after processing millions of items wastes enormous compute and money. You checkpoint progress so you resume from where you were, not the beginning.

  3. Record completed items/shards durably and key outputs by input id. When a worker is reclaimed, its shard is reassigned and resumes from the last checkpoint, skipping already-done items — so no work is lost or repeated. This resilience is exactly what makes running on cheap, interruptible spot GPUs safe.

Checkpoint progress: durably record which items/shards are done, and write outputs idempotently by input id so re-processing an item is a harmless no-op. When a worker crashes or a spot instance is reclaimed, its shard is reassigned and resumes from the last checkpoint, skipping completed items — no work lost, none repeated. This turns a giant job into something restartable, which in turn makes it safe to run on cheap, interruptible spot GPUs.

Why this piece earns its place

The knob here is granularity, and it is a pricing decision rather than a correctness one. Checkpointing every item is exact and turns one write into two, putting a second round trip on the hot path of every single prompt. Checkpointing every N items means a resume may redo up to N, which is harmless because the writes are keyed by input id — so the only thing you lose is money. Size N by what you are willing to pay for twice, not by what feels safe. Two things follow. The checkpoint store ends up the highest-write-rate component in the whole system, taking small updates continuously from every worker, and if it shares a database with the results it competes with the thing you actually care about. And “re-running an item is a no-op” holds only while generation is deterministic: with any sampling temperature the redo produces a different valid answer, so a resumed job’s outputs are not one consistent sample of the model. Fine for bulk extraction, not fine for an eval, where you pin the seed or record which rows were written on which pass.

  • checkpointdone items/shards
  • idempotentre-run = no-op
  • spot GPUscheap, interruptible

What the new pieces do

Checkpointdata
Records which items are done so a crashed/reclaimed worker resumes instead of reprocessing millions of prompts.

Step 7 · Grind it cheap

Cost optimization

The whole point of batch is tokens-per-dollar. Beyond batching, what levers cut the cost of a massive inference job?

ClientBatch APIWork QueueSchedulerInference WorkerModelResults StoreCheckpoint
The system as it stands at this step. · swipe to pan the diagram

Stack the cost levers: run on spot/preemptible GPUs (enabled by checkpointing) for a fraction of on-demand price; use provider batch APIs (many offer large discounts for latency-relaxed batch work); right-size the model (a smaller model often suffices for bulk classification/extraction — don't grind ten million prompts through a flagship if a small model passes your eval); schedule for off-peak capacity; and quantize where quality allows. Combined with high GPU utilization and prefix caching, these can cut the bill by an order of magnitude versus a naive online loop.

Why this piece earns its place

The largest cost line on a job like this is often not infrastructure at all — it is the output tokens you asked for. Generation is the expensive direction, and a template that tells the model to explain its reasoning before giving a label multiplies output length across every item in the dataset. Capping the response and asking for a bare label beats every lever above it, and it is a prompt change rather than a platform change, which means it is available on the first day. The second thing to say out loud is that these levers do not stack the way a list implies. A provider batch endpoint and your own spot fleet are alternatives, not additions: you cannot claim their discount on hardware you rent yourself, and inside their endpoint the quantization, the GPU generation and the batch size are theirs to choose. The decision underneath is buy versus run, and it turns on whether the scarce resource is GPU spend or the engineering time to operate a fleet. And when you test the smaller model, test it against labelled data, not against the flagship output. Scoring one model on its agreement with another quietly promotes the flagship to ground truth and hides every case where the two are wrong together.

  • spot GPUsfraction of on-demand
  • batch APIdiscounted
  • right-sizesmaller model if it passes

Step 8 · The sharp edges

Failures, skew & validation

At the scale of millions, rare problems become certain: transient errors, a few pathological inputs, length skew, and outputs that must be correct in bulk.

ClientBatch APIWork QueueSchedulerInference WorkerModelResults StoreCheckpoint
The system as it stands at this step. · swipe to pan the diagram

Handle failures with per-item retries (with backoff), a dead-letter path for items that keep failing (don't let a handful stall the job), and idempotent writes so retries are safe. Manage skew: a few very long inputs can dominate a batch — bucket by length and cap/split over-long inputs. Validate outputs at scale (schema/format checks, sampling for quality) since you can't eyeball millions — a malformed prompt template can silently ruin the whole run. Track cost and progress per job, and make the pipeline observable so a stuck or degrading job is caught early.

Why this piece earns its place

The retry budget has to be global, not per item. Per-item backoff absorbs a flaky minute, but a systematic failure — an expired credential, an endpoint rejecting one field of the template — fails every item identically, and a per-item policy will cheerfully re-pay for millions of rejections at full fleet throughput before anyone looks. Cap the total and halt the job when it trips. The dead-letter path needs the same treatment, because it is a queue nobody opens unless something alarms on its rate: a steady trickle is a slice of the dataset silently missing from a job that reports itself finished, and downstream nothing distinguishes a row that failed from a row never asked for. The subtler one is that schema checks pass on confidently useless output. Valid JSON carrying the label unknown on a large share of rows is green on every automated check you wrote. So alarm on the distribution of outputs against a pilot you ran before the full job — and run that pilot. A small sample through the exact template and model costs nothing next to the run it protects.

You did it

You just designed a batch inference system.

ClientBatch APIWork QueueSchedulerInference WorkerModelResults StoreCheckpoint
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Batch inference is offline, throughput-first — the opposite priorities of latency-sensitive online serving.
  • Looping the online API fails: idle GPUs, rate limits, high per-token cost, no clean restart.
  • Model it as an async job (dataset + template in, results by input id out), not per-request calls.
  • Saturate GPUs with huge + continuous batching — trade the latency you don't need for throughput.
  • Shard the dataset through a durable queue to many workers with idempotent, resumable writes.
  • Squeeze more with length bucketing (cut padding) and prefix caching (compute shared prompts once).
  • Checkpoint progress so jobs are restartable — which unlocks cheap spot GPUs, batch-discount APIs and right-sizing.

Deep cut · 27:57

It said done. Fifteen thousand were blank.

The interactive build above lays out a batch inference system: one job with a dataset, a prompt template and a model, results stored by input id, large and continuous batching to keep the GPU full, length bucketing and prefix caching, a sharded queue pulled by many workers on cheap spot GPUs, checkpoints so a crash resumes, and retries, a dead-letter path and sampled checks for the rare things that happen every night at this scale. The film builds the same system one fix at a time as a made-up market’s bakery, then replays one Sunday night to find out why a job that said two million of two million left fifteen thousand listings blank.

  • See the wreck: the scheduler marked each shard done the moment a worker took it. Two spot GPUs were reclaimed mid-shard and a third worker crashed, each holding a shard already marked; nobody took a marked shard again, and the progress count had been counting shards taken, not results written.
  • Take it into the interview: checkpoint after the durable write, never at hand-out; give every taken shard a lease (a visibility timeout) so unfinished work comes back; key writes by input id so a re-run is harmless; drain on the two-minute notice; and call the job done by counting stored results.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why is looping the online inference API a bad way to process millions of prompts?

    Because the online API is engineered for the opposite goal. Online serving minimizes latency for an individual request, so it processes small batches and returns quickly, which means calling it one item at a time leaves the GPU almost entirely idle between and within requests — massively under-utilizing the most expensive resource you have. On top of that, serial API calls run into rate limits designed for interactive traffic, they pay online pricing which is optimized for and priced around low latency (typically far more per token than batch pricing), and a naive loop over millions of items running for hours has no clean recovery: if it crashes partway through, you don't cleanly know what finished and may have to restart or reconcile by hand. Batch inference is fundamentally a different system with a different optimization target — throughput and cost per token across a whole dataset, plus resilience over a long run — so it uses large batching, sharding, checkpointing, and batch-priced or self-hosted capacity, none of which a per-request loop over the online API provides.

  2. How does trading away latency let batch inference maximize throughput?

    GPUs are massively parallel processors, and their efficiency comes from doing many computations at once; a single small request uses only a fraction of that parallelism, leaving most of the hardware idle. Online serving can't fully exploit this because it must return each user's answer quickly, which limits how large a batch it can assemble and how long it can wait to fill one. Batch inference has no per-item latency requirement — nobody is waiting on any single result — so it can push batch sizes far higher, feeding the GPU as many prompts in parallel as memory allows, and use continuous (in-flight) batching, where the moment one sequence in the batch finishes generating, a new prompt is immediately slotted into its place rather than waiting for the whole batch to complete. This keeps the GPU near full utilization despite prompts having different output lengths, instead of stalling on the slowest sequence. The result is many times more tokens processed per GPU-second — and thus per dollar — than a serial or small-batch approach. In short, you spend the latency you don't care about (any individual prompt may wait a long time) to buy the throughput you do care about (the whole dataset finishes fast and cheap).

  3. What makes a batch job restartable, and why does that matter beyond safety?

    Two things make it restartable: checkpointing progress and writing outputs idempotently. The dataset is split into shards distributed through a durable queue, workers process shards and write each result to the results store keyed by its input id, and the system durably records which items or shards are complete. Because outputs are keyed by input id, re-processing an already-done item is a harmless overwrite (a no-op in effect), and because progress is checkpointed, a worker that dies mid-shard doesn't lose the whole shard — the shard is reassigned and resumes from the last checkpoint, skipping items already finished. So no work is lost and none is needlessly repeated, even across failures in a job that runs for hours over millions of items. The reason this matters beyond mere safety is economic: this exact fault tolerance is what makes it safe to run the job on spot or preemptible GPUs, which can be reclaimed by the cloud provider at any moment but cost a fraction of on-demand instances. Without restartability, an interruption would waste or corrupt hours of work, forcing you onto expensive always-on hardware; with it, you can ride cheap interruptible capacity, so resilience directly converts into one of the largest cost savings available.

  4. What are length bucketing and prefix caching, and why do batch jobs benefit from them especially?

    Both are throughput optimizations that exploit structure batch jobs tend to have and online traffic often lacks. Length bucketing addresses padding waste: when you batch sequences of different lengths together, shorter ones are typically padded up to the length of the longest in the batch, and that padding is pure wasted compute. By sorting or grouping prompts of similar token length into the same batch (bucketing), you minimize the gap between the shortest and longest sequence, so far less compute is spent on padding. Prefix caching addresses recomputation of shared prompt content: in a batch job, many prompts often share a long common prefix — the same instruction or template, the same few-shot examples, or a shared document — and normally the model would recompute the attention key/value cache for that prefix separately for every prompt. Prefix caching computes the shared prefix's KV cache once and reuses it across all the prompts that share it, so you only pay compute for the differing suffix. Batch jobs benefit especially because they're uniform in ways interactive traffic isn't: the same task applied across millions of items means highly similar lengths and heavily shared templates, so bucketing and prefix caching each save a lot, and across millions of prompts those per-item savings compound into a large reduction in total compute and cost.

  5. How do you keep a job over millions of items correct and cost-controlled when rare problems become certain?

    At the scale of millions of items, anything with even a tiny probability per item happens many times, so the pipeline has to self-heal around problems automatically rather than assume a clean run. For failures, you retry individual items with backoff to absorb transient errors, and route items that keep failing to a dead-letter path so a handful of pathological inputs can't stall the entire job, with idempotent writes ensuring retries never double-count. For skew, you recognize that a few very long inputs can dominate a batch's time and memory, so you bucket by length and cap or split over-long inputs to keep batches balanced. For correctness, since you obviously can't eyeball millions of outputs, you validate at scale automatically — schema and format checks on every output plus sampling for quality — which is critical because a subtly broken prompt template could silently produce a million useless results before anyone notices. And throughout, you track cost and progress per job and make the pipeline observable, so a job that's stuck, degrading, or burning unexpected cost is caught early rather than after it has wasted enormous compute. The guiding principle is that at this scale "rare" effectively means "constant," so retries, dead-lettering, skew handling, automated output validation, and observability aren't optional niceties but the core of keeping the job both correct and economical.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Batch inference optimizes for…

    Nobody waits on a single result, so you trade latency for utilization and cost.

  2. Looping the online API over millions of items fails because…

    Online serving is latency-optimized; batch needs throughput + resilience — a different system.

  3. The single biggest throughput lever is…

    Fill GPU memory with parallel prompts and refill finished slots — trade latency you don't need.

  4. Checkpointing progress matters because it…

    Resume from the last checkpoint (idempotent writes) instead of reprocessing millions of prompts.

  5. Prefix caching helps batch jobs because…

    Batch jobs share templates/instructions; compute the shared prefix's KV cache once, pay only the suffix.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Submit a job: a dataset of millions of inputs + a prompt template + model choice, returning a job id.
  • Process async: no synchronous per-item response — the system runs it and reports progress.
  • Collect results: outputs land in a store keyed by input id, so the job stays resumable.
  • Grind at scale: push millions of prompts through the model fleet at maximum throughput per dollar.
  • Survive failures: a job over millions of items and hours of runtime resumes rather than restarts.

The qualities that shape everything

Each one names the mechanism that buys it.

Saturate the GPU (throughput first)
Large + continuous (in-flight) batching feeds the GPU as many prompts in parallel as memory allows and refills finished slots on the fly — no latency SLA to respect.
Scale out and survive crashes
A scheduler shards the dataset onto a durable queue; many workers pull shards and write outputs idempotently by input id, so a lost shard is simply retried.
Cut padding and shared-prompt recompute
Length bucketing groups similar-length prompts to avoid padding waste; prefix caching computes a shared template’s KV cache once and reuses it across the batch.
Survive interruption on spot GPUs
Checkpoint which items are done and write idempotently, so a reclaimed worker’s shard resumes from the last checkpoint instead of reprocessing.
Maximize tokens-per-dollar
Stack spot/preemptible GPUs, provider batch-discount APIs, off-peak scheduling and right-sizing the model — every lever trades against a latency you don’t need.
Stay correct when rare becomes constant
Per-item retries with backoff, a dead-letter path for pathological inputs, length-skew handling, and automated schema/sampling validation over millions of outputs.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

A dedicated batch system over looping the online API per item

One-at-a-time online calls leave the GPU almost idle, hit interactive rate limits, pay latency-optimized per-token prices, and can’t cleanly restart a multi-hour job. Batch inference optimizes the whole dataset for throughput and resilience instead.

Large + continuous batching over processing one prompt at a time

With no per-item latency SLA you feed the GPU as many prompts in parallel as memory allows and refill finished slots on the fly, spending latency you don’t need to keep it near 100% utilized. One-at-a-time is the single biggest waste in inference.

Checkpoint + idempotent writes over restarting the whole job on any failure

Restarting from zero after millions of items wastes enormous compute. Recording which items are done and keying outputs by input id lets a reclaimed worker’s shard resume from the last checkpoint — the resilience that makes spot GPUs safe.

Cheap interruptible spot GPUs over always-on on-demand GPUs

Always-on GPUs work but throw away the biggest cost saving. Because checkpointing makes the job tolerate interruption, you can ride spot capacity at a fraction of on-demand price.

Right-sizing the model over grinding everything through a flagship

A smaller or quantized model often passes the eval for bulk classification and extraction; running ten million prompts through a flagship when a small model suffices burns money for no quality gain — often the single biggest cost win.

The answer, out loud

What a strong answer to “Design a Batch Inference System” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–4 min

    Agree on what kind of problem this is

    Before I draw anything I want to pin down the shape of this. Nobody is waiting on any single result — we have millions of prompts and a deadline of tomorrow morning, not a user watching a cursor blink. Latency per item stops being a constraint and becomes a currency I can spend. So I’d state the goal as throughput and cost across the whole dataset, plus surviving hours of running without losing work, and I’d want to hear out loud that there really is no per-item SLA before I commit to the rest of this.

  2. 4–9 min

    Why I wouldn’t loop the online API

    The tempting first version is a loop over the dataset calling the online endpoint once per item, and I want to kill that early. It leaves the GPU almost idle, hits rate limits built for interactive traffic, pays latency-optimized prices per token, and if it dies partway through a multi-hour run there is no clean way to say what finished. The part I’d push hardest on, though, is that it is sharing capacity with real users, so a backfill degrades the product for the people who actually are waiting. Batch belongs on its own system, with the opposite optimization target.

    Built in step 1: Why looping fails
  3. 9–13 min

    A job, not N requests

    So the interface is a job. The client hands me a dataset, a prompt template and a model choice, and I hand back a job id. Everything after that is asynchronous: progress I can poll, and outputs landing in a results store keyed by input id. That framing is doing real work — it is what lets me shard, batch and checkpoint behind the API, because I’m now optimizing a whole dataset rather than each call in isolation. I’d also pin the template and the model version into the job record, so that months later I can explain why a given row looks the way it does.

    Built in step 2: The batch job model
  4. 13–19 min

    Fill the GPU

    Now the biggest lever. A GPU is massively parallel and one request barely touches it, so I’d run large batches — as many prompts in parallel as memory allows — and continuous batching on top of that, where a sequence that finishes is immediately replaced by a new one instead of the whole batch waiting on its slowest member. Online serving can’t do this, because it owes somebody an answer quickly. I can, because I don’t. That is the trade I keep coming back to for the rest of the design: I’m spending latency I don’t need to buy throughput I do.

    Built in step 3: Throughput via big & continuous batching
  5. 19–25 min

    Shard it across a fleet

    One worker won’t get through millions of items, so a scheduler splits the dataset into shards on a durable queue and a fleet of workers pulls from it. Adding workers is then how I go faster, which is the part that actually scales. The rule I’d hold onto is that every output is written keyed by its input id, so processing the same item twice is a harmless overwrite rather than a duplicate row. That one decision is what makes everything after it safe to retry — without it, every recovery mechanism I’m about to describe would be corrupting the results store instead of repairing it.

    Built in step 4: A restartable sharded pipeline
  6. 25–30 min

    Checkpoint, and buy cheap hardware with it

    On top of that I’d checkpoint progress durably — which items are done — so a worker that dies mid-shard doesn’t cost me the shard. It gets reassigned and picks up from the last checkpoint instead of starting over. The point I’d make is that this isn’t only a safety feature. Because the job now tolerates being interrupted at any moment, I can run it on spot capacity at a fraction of on-demand price, and a reclaimed instance becomes a slightly slower run rather than a lost one. The resilience is the cost strategy here, not an insurance premium against it.

    Built in step 6: Checkpointing
  7. 30–35 min

    Squeeze the batch

    With the structure in place I’d squeeze the batch itself. Length bucketing groups prompts of similar length so I’m not padding short sequences up to the longest one in the batch and paying compute for the padding. Prefix caching computes the shared template’s KV cache once and reuses it across everything that shares it, so I only pay for the part that differs. Both of those work here precisely because a batch job is the same task over and over — interactive traffic doesn’t hand you that structure to exploit.

    Built in step 5: Bucketing & prefix caching
  8. 35–39 min

    Then the bill

    Then the cost levers, which are the whole point of doing this offline: spot capacity, provider batch discounts, off-peak scheduling, quantizing where quality allows, and right-sizing the model, which is often the single biggest one. If a smaller model passes the eval for bulk classification, grinding everything through a flagship is money for nothing. I’d sequence them, though. Right-sizing and the batch discount are decisions I can take on day one, while spot capacity only becomes available to me once the checkpointing above actually works — so the resilience work has to land before the cheapest hardware is on the table.

    Built in step 7: Cost optimization
  9. 39–45 min

    The unhappy path, and the trade-off

    Last, the sharp edges, because at millions of items anything rare is constant. Per-item retries with backoff, a dead-letter path so a handful of pathological inputs can’t stall the job, bucketing and capping for length skew, and automated validation — schema checks plus sampling — because I can’t eyeball millions of outputs and a broken template ruins a run quietly. The trade-off is the same throughout: everything here buys throughput and cost with latency I don’t need, and every one of those choices would be wrong for an online service. With more time I’d work on shard sizing, since one oversized shard leaves the whole fleet waiting on a single worker at the end of the run, and on what the job should promise a caller when it finishes only partly done.

    Built in step 8: Failures, skew & validation

What this teaches

Learn AI system design by building an offline batch LLM inference system that processes millions of prompts step by step. An interactive guide covering why looping the online API fails, optimizing throughput over latency, a restartable sharded pipeline, batching techniques (continuous batching, length bucketing, prefix caching), scaling on spot GPUs with checkpointing, cost optimization, and the unhappy paths (failures at scale, idempotency, skew).

Key takeaways

  • Batch inference is offline, throughput-first — the opposite priorities of latency-sensitive online serving.
  • Looping the online API fails: idle GPUs, rate limits, high per-token cost, no clean restart.
  • Model it as an async job (dataset + template in, results by input id out), not per-request calls.
  • Saturate GPUs with huge + continuous batching — trade the latency you don't need for throughput.
  • Shard the dataset through a durable queue to many workers with idempotent, resumable writes.
  • Squeeze more with length bucketing (cut padding) and prefix caching (compute shared prompts once).
  • Checkpoint progress so jobs are restartable — which unlocks cheap spot GPUs, batch-discount APIs and right-sizing.

Concepts covered

  • What is batch inference?
  • Why looping fails
  • The batch job model
  • Throughput via big & continuous batching
  • A restartable sharded pipeline
  • Bucketing & prefix caching
  • Checkpointing
  • Cost optimization
  • Failures, skew & validation
RUN IT YOURSELF

The slot that kept computing after its answer was already finished

One shard of a batch job — 96 prompts, 8 decode slots, one GPU — scheduled three ways and simulated decode step by decode step, running for real in your browser. Identical model, identical prompts, identical tokens out; it prints how long the GPU was held, how many forward-pass rows that cost, how much of that was padding, and how late each length population was against its own solo time. Change LONG_SHARE, hit Run, and watch the waste move with the length spread.

HOW TO READ THE CODE — 5 IDEAS
  1. A static batch is a rectangle: every row is computed every iteration until the longest sequence in it stops, so 73.0% of the rows go to sequences that already finished (step 3). Across seeds that figure never leaves 62–78%.
  2. The same 8 slots finish the shard in 3.05x less GPU time under continuous batching — 3004 iterations against 985 (step 3). That multiplier runs 2.0x–4.1x across seeds; the shipped seed is the median one.
  3. Every member of a static batch returns on the same iteration, so the one shared wait is 36x a short request’s own decode time and 6x a long one’s. Continuous cuts the short figure to 7x by returning each sequence the pass it finishes — that is length skew (step 8).
  4. Sorting the shard by generation length lands at 0.99x of continuous — within ±15% of it across seeds, and ahead of it on nearly half. So the cost is the length spread, not the scheduler’s name (step 5) — but the key is the generation length, which does not exist until you have generated it.
  5. The 0.0% padding column is true by construction, not measured: continuous only charges resident rows. The iteration counts are the claim — flat lengths make both schedulers cost the same, and widening SLOTS makes the rectangle worse (82.2% at 32), not better.
CPython · WebAssembly
built to grind ten million prompts through a GPU fleet overnight — make the calls, reclaim a worker, run the gauntlet.
Finished this one? 0 / 65 AI System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More AI System Designs