What Is Continuous Batching?

Continuous batching

Continuous batching is a way of serving a large language model (LLM) where requests join and leave the running batch at every generation step. It matters because it keeps GPUs busy under mixed request lengths, which raises throughput and cuts the time short requests spend waiting behind long ones.

For example, suppose a server runs eight requests together. Seven need 20 output tokens and one needs 400. With static batching, all eight slots stay occupied for 400 steps, and only 540 of the 3,200 slot-steps do useful work. With continuous batching, the seven short requests leave after step 20 and queued requests take their slots right away.

Continuous batching is also called iteration-level batching. The model does not change. Only the scheduling does.

Try Arize AX

Build better agents with Arize

Trace, evaluate, and learn. Build agents that work with Arize AX and start tracing your runs today.

Prefer open source? Try Arize Phoenix for self-hosted, open source agent observability.

Why continuous batching matters

Interactive LLM traffic rarely arrives in neat groups of equal length. Some users want a one-line answer and others want a long report.

When a server waits for a whole batch to finish, short requests pay for the long ones. Continuous batching removes that wait.

Continuous batching is useful for:

  • Raising throughput: Keep batch slots filled with active requests instead of finished ones.
  • Reducing wait for short requests: Return each request as soon as it finishes and admit the next one.
  • Serving live traffic: Handle requests that arrive at different times with different output lengths.
  • Comparing serving setups: Understand why two servers running the same model report very different throughput and latency.

How continuous batching works

Autoregressive generation runs in steps. First, a prefill phase processes the prompt. Then, during decoding, the model produces roughly one token per request per step.

Because autoregressive models generate token by token, a server can change which requests are in the batch between steps. The Hugging Face Transformers documentation on continuous batching describes scheduling at this step-level granularity.

Static batching

With static batching, the server assembles a group of requests and runs them together. It returns results only when the slowest request finishes its last token.

Consider the opening example. If seven requests need 20 tokens and one needs 400, seven slots sit occupied doing padded or idle work for most of the run.

Continuous batching

With continuous batching, the scheduler re-forms the active set at every step. Finished sequences are returned as soon as they end. Waiting requests are admitted as capacity allows.

Step t:    [A, B, C, D]   B finishes -> return B immediately
Step t+1:  [A, C, D, E]   E (queued) joins after its prefill
Step t+2:  [A, C, D, E]   ... until each sequence completes

Here, request B leaves the moment it finishes. Request E, which was waiting in the queue, takes the free slot after its prefill runs.

What still limits admission

A free slot does not always mean a new request can start. The server also needs memory in its key-value (KV) cache to hold the new request’s state.

When the cache is full, requests may be delayed, preempted, or recomputed. Long prompts can also stall other requests during prefill unless the server splits prefill work into chunks. For more on that memory, see KV cache.

Simulate static and continuous batching in Python

The following script compares the two schedules in a simplified model. It uses the opening example plus eight more 20-token requests waiting in the queue, all hypothetical.

The model ignores prefill and treats every decode step as equal cost. Real servers do not behave this simply, so read the output as an illustration of scheduling, not a benchmark.

from collections import deque

BATCH_SIZE = 8
first_wave = [400] + [20] * 7   # one long request, seven short ones
waiting = [20] * 8              # eight short requests already queued

def static_batching(requests):
    finish, step = [], 0
    for i in range(0, len(requests), BATCH_SIZE):
        batch = requests[i:i + BATCH_SIZE]
        step += max(batch)               # the batch waits for its longest request
        finish += [step] * len(batch)
    return finish

def continuous_batching(requests):
    queue = deque(enumerate(requests))
    active, finish, step = {}, [None] * len(requests), 0
    while queue or active:
        while queue and len(active) < BATCH_SIZE:
            idx, length = queue.popleft()
            active[idx] = length
        step += 1                        # one decode step for every active request
        for idx in list(active):
            active[idx] -= 1
            if active[idx] == 0:
                finish[idx] = step
                del active[idx]
    return finish

requests = first_wave + waiting
for name, fn in [("static", static_batching), ("continuous", continuous_batching)]:
    done = fn(requests)
    queued = done[len(first_wave):]
    print(f"{name:<11} all done at step {max(done)}, "
          f"queued requests finish on average at step {sum(queued) / len(queued):.1f}")

useful = sum(first_wave)
held = BATCH_SIZE * max(first_wave)
print(f"static first batch: {useful} useful slot-steps of {held} ({useful / held:.1%})")

Output:

static      all done at step 420, queued requests finish on average at step 420.0
continuous  all done at step 400, queued requests finish on average at step 42.5
static first batch: 540 useful slot-steps of 3200 (16.9%)

Worked example: where the queued requests finish

Here is what happens to the eight queued requests in the simulation above.

Schedule When queued requests start Average finish step for queued requests All 16 requests done
Static batching After the 400-token request ends 420.0 Step 420
Continuous batching As soon as a slot frees up 42.5 Step 400

Under static batching, the queued requests wait 400 steps for the long request, then take 20 more steps each. They all finish at step 420.

Under continuous batching, seven queued requests take the free slots after step 20 and finish at step 40. The eighth starts once a slot frees up again and finishes at step 60. Their average is 42.5.

The 400-token request finishes at step 400 in both cases. Continuous batching does not make a long generation faster. It reduces wasted capacity around it.

How to measure continuous batching

Compare serving configurations at the same concurrency, prompt-length mix, and output-length mix. Report more than one number.

  • Throughput: Completed tokens or requests per second over the measurement window. For more, see LLM throughput.
  • Latency distribution: Time to first token and per-token latency at the median and at high percentiles, not only the mean. Define where timing starts and stops before measuring.
  • Queue time: How long requests wait before prefill. Rising queue time with flat throughput usually means the server is saturated, not inefficient.
  • Batch occupancy and KV cache pressure: How full the active batch is, and whether requests are delayed, preempted, or recomputed because cache space ran out.

How to debug latency with continuous batching

Instrument the client and, where the server exposes it, the scheduler. Each view answers a different question.

Application traces show timing from the caller’s side. The Arize AX documentation and Arize Phoenix documentation cover tracing the served application.

Server metrics explain whether time went to queueing, prefill, or decode. Relate the two views using AI observability practices. Do not treat one request’s latency as a server benchmark.

When a request passes through several services before reaching the model, keep the full path in view with distributed tracing.

Limitations of continuous batching

Continuous batching removes one source of waiting. Several others remain.

  • Long generations: An individual long request takes as many steps as before.
  • Prefill stalls: Very long prompts can delay other requests during prefill unless the server chunks prefill work.
  • Admission fairness: Admission policies decide which tenant or request class gets slots first, and that is not always fair.
  • Overload: Throughput may hold while high-percentile latency climbs. A fuller batch is not automatically a healthier service.
  • Portability: Framework, scheduling policy, maximum batch size, and cache limits all change results. Pin them when you report a comparison.

Continuous batching vs. static batching and Batch APIs

Static batching is simpler. It can be entirely adequate offline, where requests arrive together and nobody waits interactively. Continuous batching suits live traffic with varied generation lengths.

A provider Batch API is a different product. You submit a bulk job, accept a long completion window, and receive results later. It optimizes cost for asynchronous work and involves no step-level scheduling of your live requests.

Confusing these leads to bad expectations. Enabling a Batch API will not improve interactive latency. Continuous batching will not give you bulk-job pricing.

Frequently asked questions

Does continuous batching change model output?

No. It changes when requests run together, not the computation for a given request. Outputs should match for the same model and decoding settings, apart from ordinary numerical variation in batched execution.

Why is tail latency still poor with continuous batching?

A request can still wait in the queue, wait behind long prefills, or compete with many concurrent decodes. Continuous batching removes the static-batch wait, which is only one cause.

Is continuous batching the same as a Batch API?

No. Continuous batching schedules live requests inside a server at each generation step. A Batch API accepts bulk jobs for later, asynchronous completion.

Does continuous batching make a single request faster?

Not on its own. A long generation needs the same number of decode steps. The gain comes from not making short requests wait for it.

What should I report when comparing LLM servers?

Report concurrency, prompt and output length distributions, throughput, time to first token, per-token latency percentiles, and the scheduler and cache limits you used. A single tokens-per-second figure hides the tradeoffs.

Putting continuous batching to work

Before you compare servers, fix the traffic mix you test with. Use the same concurrency, prompt lengths, and output lengths for every configuration.

Record throughput, queue time, time to first token, and per-token latency at p50 and p95. Note the framework, scheduling policy, maximum batch size, and cache limits next to the results.

When latency rises under load, check queue time and KV cache pressure before blaming the model. Tracing your application with Arize Phoenix shows the caller’s view, which you can line up against the server’s own scheduler metrics.

Example

A server runs eight requests together: seven need 20 output tokens and one needs 400. With static batching, all eight slots stay occupied for 400 steps and only 540 of 3,200 slot-steps do useful work. With continuous batching, the seven short requests leave after step 20, and in a simplified simulation eight queued requests finish at step 42.5 on average instead of step 420.

Don’t ship vibes.

Arize gives AI teams observability and evals to understand and improve agent performance.