Infrastructure

Routing, load balancing, queueing

A GPU sitting idle between requests is money burning. Smart routing, load balancing, and request queueing are the glue that keeps every accelerator fed and every user happy.

Once you have more than one GPU replica serving a model, the question stops being "how do I run inference?" and becomes "how do I send each request to the right replica?" This sounds simple until you realize that LLM requests are wildly heterogeneous: a 50-token completion finishes in milliseconds, while a 4096-token generation holds a slot for seconds. A naive round-robin balancer treats them identically, and the result is predictable: some replicas are swamped while others idle.

Today I want to walk through three layers of the problem: routing strategies, load balancing algorithms, and request queueing. Together they determine whether your inference cluster actually uses the GPUs you're paying for.

Layer 1: Routing strategies

At the highest level, routing decides which pool of replicas a request should target. In a production LLM deployment, there are several reasons you might want more than one pool:

Prefix-aware routing deserves special attention. In systems like vLLM with prefix caching enabled, the KV cache for a shared system prompt is computed once and reused across requests. If your load balancer scatters requests randomly across replicas, every replica computes the same prefix independently. A hash-based routing scheme (hash the system prompt, map to a replica) keeps prefix-sharing replicas hot.

# Prefix-aware routing: hash the system prompt to pick a replica
import hashlib

def route_by_prefix(system_prompt: str, num_replicas: int) -> int:
    h = hashlib.sha256(system_prompt.encode()).hexdigest()
    return int(h, 16) % num_replicas

The tradeoff: sticky routing concentrates load. If one system prompt dominates traffic, its target replica becomes a hotspot. A good system combines prefix-awareness with a fallback: try the preferred replica, but overflow to others if its queue depth exceeds a threshold.

Layer 2: Load balancing algorithms

Once a request reaches a pool, the load balancer picks a specific replica. Here are the strategies I've seen used in practice, ordered by sophistication:

In practice, I've found that least-connections with a health-check heartbeat handles most workloads well enough, and the jump from round-robin to least-connections is much larger than the jump from least-connections to anything fancier. Start simple.

Layer 3: Request queueing

When all replicas are busy, requests have to wait somewhere. The queue design matters more than most people think:

# Simple priority queue with admission control
import heapq, time

class InferenceQueue:
    def __init__(self, max_depth=100):
        self.heap = []
        self.max_depth = max_depth
        self.counter = 0

    def enqueue(self, request, priority=1):
        if len(self.heap) >= self.max_depth:
            raise QueueFullError("HTTP 429: queue at capacity")
        # Lower priority number = higher priority
        heapq.heappush(self.heap, (priority, self.counter, time.time(), request))
        self.counter += 1

    def dequeue(self):
        if not self.heap:
            return None
        return heapq.heappop(self.heap)

The interaction with continuous batching

One thing that makes LLM load balancing different from traditional web services is continuous batching. In a continuously-batched engine like vLLM, a replica doesn't process requests one at a time. It dynamically adds new requests to the running batch as old ones finish. This means the "load" on a replica isn't just "number of requests" but "how much KV cache memory is allocated" and "how many tokens remain to generate."

A smart load balancer for continuous batching should factor in the replica's available KV cache slots, not just its in-flight request count. If a replica has 90% of its KV cache allocated, sending it another long-context request will cause it to preempt (evict and recompute) existing requests, hurting everyone's latency.

Practical advice

Expose KV cache utilization in your /health endpoint. A load balancer that routes away from replicas above 80% KV cache utilization will avoid preemption cascades. vLLM's metrics endpoint already exports vllm:gpu_cache_usage_perc for exactly this purpose.

Putting it together

A production-grade routing stack for LLM inference typically looks like this:

The goal is always the same: keep every GPU's batch full without letting any single request wait too long. It's a balancing act between throughput (fill the batch) and latency (serve quickly), and the right tradeoff depends on your SLO.

Next: multi-cloud capacity, where routing crosses cloud boundaries and the tradeoffs get even more interesting.