Continuous Batching: Optimizing LLM Inference Throughput › Throughput Analysis
Language AI HandbookAll chapters Continuous Batching: Optimizing LLM Inference Throughput › Throughput AnalysisOne section · Sections in this chapter7 / 14 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's compare the throughput of static and continuous batching quantitatively. Understanding the mathematical relationship between these approaches helps explain why continuous batching delivers such significant improvements and under what conditions those improvements are largest.
The analysis that follows formalizes the intuition we built earlier: static batching allocates computational capacity based on the worst case in each batch, while continuous batching adapts to the actual workload token by token. The efficiency gap between these strategies grows with the variance of output lengths, which is exactly what characterizes real LLM workloads.
Theoretical Analysis
To understand the efficiency difference mathematically, we need to formalize what happens in each batching strategy. The key insight is that static batching allocates computational capacity based on the worst case, while continuous batching adapts to the actual workload.
For static batching with a fixed batch size containing requests with output lengths , the total computational time is determined by the longest request. Every request in the batch must wait for this slowest member, regardless of whether it has long since completed. The total computational capacity allocated, measured in token-slots (one slot processing one token), is:
where:
- : the total number of processing slots consumed (iterations batch size)
- : the fixed batch size
- : the output length of the -th request
- : the length of the longest sequence in the batch
This formula captures the basic problem: we allocate capacity based on the maximum, not the sum. Every slot runs for as many iterations as the longest sequence demands, even if that slot has been creating padding for 90% of those iterations.
We can calculate the compute efficiency as the ratio of useful work to total capacity. Useful work is the actual tokens generated, while total capacity is the processing slots allocated:
where:
- : the computational efficiency (between 0 and 1)
- : the total number of generated tokens (useful work)
- : the total capacity allocated during the batch's lifetime
Notice that efficiency depends heavily on the distribution of output lengths. If all requests have the same length, the numerator equals the denominator and efficiency is 100%. But if one request is much longer than the others, efficiency drops sharply.
To make this concrete, consider a batch of 8 requests where 7 need 20 tokens and 1 needs 200 tokens. We can work through the efficiency calculation step by step:
The system performs 1600 computational operations but only 340 of them produce useful output. That is 79% wasted compute, or a 21% efficiency rating. The single outlier request forces seven other requests to continue burning resources for 180 extra iterations each.
For continuous batching, the situation is fundamentally different. Assuming the request queue remains populated (perfect slot filling), the system avoids waiting for long requests. When a request completes, its slot immediately fills with a new request. The total iterations required approach the theoretical minimum:
where:
- : the approximate total iterations needed
- : the total number of requests in the workload
- : the output length of the -th request
- : the maximum batch size (concurrency limit)
- : the total number of tokens to be generated across all requests
This formula shows that continuous batching distributes work evenly across the available batch slots. The total iterations is simply the total tokens divided by the parallelism factor. Doubling halves the time, and adding more requests to a non-empty queue barely changes the total time because they fill slots freed by completions.
We can derive the efficiency by comparing useful work to allocated capacity:
This shows that efficiency approaches 100% because the total capacity used matches the useful work. Every slot-iteration produces a real output token rather than padding. The approximation becomes exact when the queue is always full and requests are perfectly divisible across slots.
The practical implication is that continuous batching eliminates the sensitivity to output length distribution. Whether requests generate 10 tokens or 1000, the throughput stays near the theoretical maximum. Static batching is at the mercy of whoever happens to be in the same batch as you, while continuous batching processes everyone at their natural speed.
Empirical Comparison
Let's run a more complete simulation comparing the two approaches across a realistic workload. Theory tells us continuous batching should win, but simulation reveals the magnitude of improvement and how it varies with workload characteristics.
def simulate_workload(
num_requests: int, batch_size: int, output_lengths: List[int], method: str
) -> dict:
"""Simulate a workload with either static or continuous batching."""
if method == "static":
# Process in fixed batches
total_iterations = 0
total_latency = 0
for batch_start in range(0, num_requests, batch_size):
batch_end = min(batch_start + batch_size, num_requests)
batch_lengths = output_lengths[batch_start:batch_end]
max_len = max(batch_lengths)
total_iterations += max_len
# All requests in batch have same completion time
for length in batch_lengths:
total_latency += max_len # Wait for slowest
return {
"total_iterations": total_iterations,
"average_latency": total_latency / num_requests,
"throughput": num_requests / total_iterations,
}
else: # continuous
# Simulate continuous batching
remaining = list(output_lengths)
active = []
waiting_idx = 0
iteration = 0
completion_times = []
while active or waiting_idx < len(remaining):
# Fill batch with waiting requests
while len(active) < batch_size and waiting_idx < len(remaining):
active.append(
{
"idx": waiting_idx,
"remaining": remaining[waiting_idx],
"start": iteration,
}
)
waiting_idx += 1
if not active:
break
iteration += 1
# Decrement all active and check completions
still_active = []
for req in active:
req["remaining"] -= 1
if req["remaining"] <= 0:
completion_times.append(iteration - req["start"])
else:
still_active.append(req)
active = still_active
return {
"total_iterations": iteration,
"average_latency": np.mean(completion_times),
"throughput": num_requests / iteration,
}# Generate a realistic workload with variable output lengths
num_requests = 100
batch_size = 8
# Output lengths following a log-normal distribution (common in real workloads)
output_lengths = np.random.lognormal(
mean=4.0, sigma=0.8, size=num_requests
).astype(int)
output_lengths = np.clip(output_lengths, 10, 500).tolist()
static_results = simulate_workload(
num_requests, batch_size, output_lengths, "static"
)
continuous_results = simulate_workload(
num_requests, batch_size, output_lengths, "continuous"
)Workload: 100 requests, batch size 8 Output length range: 10 - 448 tokens Mean output length: 77.0 tokens Static Batching: Total iterations: 2585 Average latency: 202.5 iterations Throughput: 0.039 requests/iteration Continuous Batching: Total iterations: 1107 Average latency: 77.0 iterations Throughput: 0.090 requests/iteration Improvement: Throughput gain: 2.34x Latency reduction: 62.0%
Continuous batching reaches materially higher throughput and lower average latency. While the static batch is held back by the longest sequence in each group, continuous batching processes requests at their natural speed, returning short responses quickly while long ones continue processing.
# Test with different variance levels
variances = [0.2, 0.5, 0.8, 1.2]
static_throughputs = []
continuous_throughputs = []
static_latencies = []
continuous_latencies = []
for var in variances:
lengths = np.random.lognormal(mean=4.0, sigma=var, size=100).astype(int)
lengths = np.clip(lengths, 10, 500).tolist()
static = simulate_workload(100, 8, lengths, "static")
continuous = simulate_workload(100, 8, lengths, "continuous")
static_throughputs.append(static["throughput"])
continuous_throughputs.append(continuous["throughput"])
static_latencies.append(static["average_latency"])
continuous_latencies.append(continuous["average_latency"])

The improvement from continuous batching grows with output length variance. When all requests generate similar lengths, static batching loses less to waiting because the maximum is close to the average. But real workloads have high variance: some users ask for single-sentence summaries while others request long document analyses. This variance makes continuous batching useful in practical deployments.