Why Agentic Workloads Are the Killer App for KV Cache-Aware Routing

Your agent makes 7 LLM calls per user query. Without smart routing, you're paying for 7 cold prefills. I did the math on what that costs you, and dug into how llm-d fixes it.

MR
Markell Rawls
writes about inference infrastructure
TL;DR

AI agents make 5 to 30+ LLM calls per user request, and every call after the first recomputes the same system prompt and tool definitions from scratch. llm-d kills that waste by routing all of an agent's calls to the same GPU pod, where the shared prefix is already cached. The result: 70% less prefill latency, 2.6 seconds saved per agent execution, and roughly $4,200/month in GPU savings for a mid-scale deployment. Basically, if you're building agents without cache-aware routing, you're paying for the same computation 7 times per request.

Key Takeaways
70%
1,107ms with llm-d vs 3,750ms without
Total prefill time across 8 agent calls drops by 70%. That's 2.6 seconds of latency gone from every single agent execution.
7x
7 cold prefills per agent request = 2.8s of pure waste
After the first call, every subsequent LLM call recomputes the same system prompt and tool definitions from scratch. That's GPU time spent doing nothing new.
44
At 1,000 concurrent agents: 44 GPU-minutes of freed compute per minute
Nearly 44 GPUs' worth of redundant prefill work just disappears once routing gets smart about it. At cloud pricing, that's thousands of dollars a month.

Agent Execution: Before and After llm-d

Say a customer support agent gets "I need to return the shoes I ordered last week." Here's what actually happens at the inference layer, step by step:

Without llm-d (Random Routing)
Every call is a cold start
1
Parse user intent
445ms prefill
2
Look up recent orders
445ms prefill
3
Interpret order data
454ms prefill
4
Check return policy
463ms prefill
5
Interpret policy
472ms prefill
6
Initiate return
481ms prefill
7
Confirm success
490ms prefill
8
Compose response
500ms prefill
Total prefill
3,750ms
With llm-d (Cache-Aware Routing)
One cold start, seven cache hits
1
Parse user intent
445ms prefill (cold)
2
Look up recent orders
67ms prefill
3
Interpret order data
76ms prefill
4
Check return policy
85ms prefill
5
Interpret policy
95ms prefill
6
Initiate return
104ms prefill
7
Confirm success
113ms prefill
8
Compose response
122ms prefill
Total prefill
1,107ms

In This Article

  1. Why Agentic Workloads Are the Killer App
  2. The Prefix Cache Advantage
  3. Multi-Turn Conversation Routing
  4. Multi-Agent Cache Sharing
  5. MCP and llm-d
  6. Latency Analysis
  7. Cost Impact
  8. The Agentic Scaling Challenge
  9. Agentic Routing Challenges
  10. Observability for Agents
  11. Agentic Architecture Patterns
  12. Pattern Comparison at a Glance
  13. Which Pattern Should I Use?
  14. Production Patterns for Agentic Inference
  15. Further Reading

Why Agentic Workloads Are the Killer App for KV Cache-Aware Routing

Single-shot LLM calls are on their way out. The pattern that dominated 2023 and early 2024 (send a prompt, get a completion, done) is giving way to something way more demanding: agentic workloads. An agent doesn't make one LLM call per user request. It makes five. Ten. Sometimes thirty or more. Each call is a step in a reasoning chain: figure out what the user wants, pick a tool, parse the tool's output, decide the next action, write the final response, and sanity-check its own work along the way.

This is the shift that turns KV cache-aware routing from "nice to have" into "you really need this." With a simple chatbot, a cold cache miss on a single call adds maybe 400 milliseconds of extra prefill. Annoying, but fine. With an agent chaining eight LLM calls sequentially, that 400ms penalty compounds. You're not adding latency to one response — you're adding it to every step of the reasoning loop.

5-30+
LLM calls per agent request
~400ms
Cold prefill overhead per call
4+ sec
Wasted on cold starts (10 calls)

Here's a concrete example. A customer support agent gets: "I need to return the shoes I ordered last week." The execution looks something like: (1) parse the user intent, (2) call a tool to look up recent orders, (3) interpret the order data, (4) call a tool to check the return policy, (5) interpret the policy, (6) call a tool to initiate the return, (7) confirm the return was successful, (8) compose a final response to the user. That's eight LLM calls, and every one of them includes the same system prompt and tool definitions as its prefix. Without cache-aware routing, every single call lands on a random pod and pays the full prefill cost for the entire shared prefix.

The math is pretty damning. If each call carries roughly 2,300 tokens of shared prefix (a typical system prompt plus tool definitions), and your average prefill throughput is around 5,000 to 6,000 tokens per second on a well-utilized GPU, you're spending about 400ms just recomputing the KV cache for that prefix on every call. Multiply by eight calls: 3.2 seconds of pure waste. The user stares at a spinner for three extra seconds — not because the model is slow, but because the infrastructure keeps redoing work it already did.

Key Insight

The Compound Latency Effect

In a single-call LLM app, a cache miss adds a few hundred milliseconds — barely noticeable. In agentic workloads, that penalty compounds across every step of the reasoning chain. Ten calls with 400ms of overhead each is 4 seconds of wasted compute. At scale, that's the difference between a snappy agent and one that feels broken.

This is exactly the problem llm-d solves. llm-d is a CNCF Sandbox project built for distributed LLM inference routing. It sits in front of your vLLM backends and uses its EPP (Endpoint Picker Policy) component to make routing decisions. When an agent's first call lands on a pod and computes the KV cache for its prefix, llm-d makes sure all subsequent calls from that agent go to the same pod. The prefix is already cached. Calls two through eight skip prefill entirely for the shared tokens and only compute attention for the new, call-specific tokens.

And the result is honestly kind of dramatic. Instead of 3.2 seconds of redundant prefill across eight calls, you pay 400ms once on the first call and roughly 65ms on each call after (for the small amount of new, call-specific context). Total prefill drops from 3.2 seconds to 855 milliseconds — a 73% reduction. That's not a micro-optimization. That's a different user experience.

The Prefix Cache Advantage: System Prompts and Tool Definitions as Shared Prefixes

To see why llm-d's approach works so well for agents, it helps to know how KV caches work at the GPU level and why prefixes are the natural unit of caching.

When a transformer processes a prompt, it computes key and value tensors for every token at every attention layer. These tensors are basically the model's "memory" of the tokens it has seen — they encode the contextual relationships between tokens that the model needs to generate output. For something like Llama 3.1 70B with 80 attention layers and a hidden dimension of 8192, each token produces a KV pair that eats a non-trivial amount of GPU memory and, more importantly, a non-trivial amount of compute to generate.

The key thing: KV cache entries are deterministic for a given prefix. If two requests share the same first 2,000 tokens, their KV cache entries for those 2,000 tokens are byte-for-byte identical. There's no randomness in the attention computation during prefill — the model computes the exact same key and value tensors no matter what comes after the prefix. So once you've computed the KV cache for a prefix on a specific GPU, any request that starts with that same prefix can reuse the cached tensors and skip straight to computing attention for the new tokens.

Why System Prompts and Tool Definitions Are Ideal for Caching

In agent architectures, the shared prefix isn't some small header or boilerplate. It's big, and it's the most expensive part of the prompt to process because it comes first and sets up the context window for everything after it.

A typical agent system prompt runs between 500 and 2,000 tokens. It defines the agent's persona, behavioral constraints, output formatting rules, and domain-specific instructions. This isn't a one-line "You are a helpful assistant." It's a detailed spec that might look like:

You are a senior customer support agent for Acme Corp.
You handle returns, exchanges, and order inquiries.

BEHAVIORAL RULES:
- Always verify the customer's identity before accessing order data
- Never disclose internal pricing or margin information
- Escalate to a human agent if the customer expresses frustration
  three or more times
- Always confirm actions before executing them
...
(continues for 800+ more tokens of guardrails, tone guidance,
 output format specifications, and escalation policies)

On top of the system prompt you've got the tool definitions — the structured schemas that tell the model what tools it can invoke, what parameters each takes, and what each returns. For an agent with five or six tools, these easily add another 200 to 1,000 tokens. Each definition has a name, description, parameter schemas with types and descriptions, required fields, and often example invocations or edge-case guidance.

Concrete Example

Agent prefix breakdown:
System prompt: 1,500 tokens
Tool definitions (6 tools): 800 tokens
Total shared prefix: 2,300 tokens

Without llm-d, every call recomputes attention for all 2,300 prefix tokens. With llm-d, call 1 computes the prefix and caches it. Calls 2 through N skip straight to the call-specific context — usually just 50 to 200 tokens of new conversation or tool output. That's a 90%+ reduction in prefill work per call.

The savings get even bigger with larger models. Prefill compute scales linearly with both token count and model size. A 2,300-token prefix on a 70B model takes roughly 400ms to prefill. On a 405B model, that same prefix might take over a second. For agents on frontier models, redundant prefix computation isn't just an annoyance — it's the dominant latency bottleneck.

llm-d's EPP tracks which pods have computed KV caches for which prefixes. When a new request comes in, the EPP looks at its prefix and checks whether any active pod already has it cached. If yes, route there. If not, pick a pod using load-aware heuristics and mark it as the new cache holder for that prefix. The lookup is fast (microseconds) because the EPP keeps an in-memory index of prefix hashes to pod assignments.

Multi-Turn Conversation Routing: Keeping Full Conversation Context Cached Across Turns

The prefix advantage goes way beyond the initial system prompt and tool definitions. In multi-turn conversations (which is basically every agentic workload), each turn adds to the prefix. Turn 1's prompt is the system prompt plus tool definitions plus the user's first message. Turn 2's prompt is all of that, plus the model's first response, plus the user's second message. By turn 5, the "prefix" shared with the previous turn might be 6,000 tokens or more.

This is where sticky routing really matters. llm-d supports session-aware routing that keeps an entire conversation pinned to the same pod across turns. The mechanics are simple: the first request in a conversation gets routed to a pod based on prefix matching or load balancing. llm-d then associates the conversation's session identifier with that pod. Every later request carrying the same session ID goes to the same pod, where the growing KV cache from previous turns is already sitting in GPU memory.

Compounding Returns

The Growing Prefix Effect

Each turn of a conversation grows the cached prefix. On turn 1, you cache 1,500 tokens (system prompt + tools). By turn 3, maybe 3,500 tokens. By turn 5, 6,000. The savings compound each turn because you're skipping more prefill work each time. So the longer a conversation runs, the more cache-aware routing is worth.

The numbers make the case on their own. Take a five-turn agent conversation where each turn involves two LLM calls (one to decide on an action, one to compose the response):

Turn Cached Prefix Tokens TTFT Without llm-d TTFT With llm-d Savings
Turn 1, Call 1 0 (cold start) 450ms 450ms n/a
Turn 1, Call 2 1,500 450ms 75ms 375ms
Turn 2, Call 1 2,200 480ms 60ms 420ms
Turn 2, Call 2 2,800 500ms 55ms 445ms
Turn 3, Call 1 3,500 530ms 50ms 480ms
Turn 3, Call 2 4,000 550ms 48ms 502ms
Turn 4, Call 1 4,800 575ms 45ms 530ms
Turn 4, Call 2 5,400 600ms 42ms 558ms
Turn 5, Call 1 5,700 610ms 41ms 569ms
Turn 5, Call 2 6,000 620ms 40ms 580ms

Without sticky routing, each of these calls could land on a different pod and pay full prefill cost every time. The later turns hurt even more, because the growing conversation history means more tokens to prefill from scratch. With llm-d, the first call eats the cold-start cost and every call after rides the growing cached prefix — later turns save the most.

One subtlety worth calling out. When a conversation's KV cache grows big enough, it can exceed what a single pod can keep resident in GPU memory alongside other active requests. llm-d handles this with cache eviction policies. If a conversation's cache gets evicted under memory pressure, llm-d can either re-route the conversation to a pod with more available cache memory, or eat the one-time recomputation cost and re-cache on the same pod. Because the EPP knows per-pod memory utilization, these decisions get made with full knowledge of cluster state instead of blindly sticking to a pod that no longer holds the cache.

Multi-Agent Cache Sharing: How Agents with the Same Tools Benefit from Shared Routing

So far this has all been about cache reuse within a single agent's execution or conversation. But the part that really got me is cache sharing across different agents.

In real platforms, it's common to have multiple agent types sharing overlapping tooling. A Research Agent, a Code Agent, and a Customer Support Agent might all have access to a web_search tool, a knowledge_base_query tool, and a send_notification tool. Their system prompts differ, but their tool definition blocks may be partially or fully identical. That's a cross-agent caching opportunity that traditional load balancers completely miss.

llm-d's prefix matching isn't all-or-nothing — it works at a granular level. If two agents share the first 800 tokens of their prompts (identical tool definitions) but diverge after that (different system prompts), llm-d can still route them to the same pod to take advantage of that 800-token shared prefix. The KV cache for those 800 tokens gets reused, and only the divergent portion is computed fresh. This partial prefix matching is one of the cleverer parts of llm-d's EPP implementation, and it's what makes multi-agent cache sharing actually practical.

Multi-Tenant Bonus

Shared Prefixes Across Customer Agents

Picture a SaaS platform hosting AI agents for 50 different customers. Each customer has a unique system prompt, but all 50 agents use the same set of tools (CRM lookup, email send, calendar check, knowledge base search). The tool definitions alone might be 900 tokens of shared prefix. llm-d can route requests from all 50 agents to a set of pods that keep those tool definitions cached, saving 900 tokens of prefill on every single call across all customers.

At 1,000 requests per second across all tenants, that's 900,000 tokens per second of redundant prefill gone. At typical GPU prefill throughput, you're freeing up the equivalent of 150+ GPU-seconds of compute every second — a big chunk of the cluster's total capacity back in play.

This multi-agent cache sharing gets especially good on platforms doing the "tool marketplace" thing, where agents get composed from a library of standardized tools. The more standardized your tool definitions, the more cache sharing llm-d can exploit. Nice feedback loop there: standardizing common tool schemas doesn't just make developers' lives easier, it directly cuts inference costs through better cache utilization.

There's an architectural implication here worth sitting with. Traditional load balancers treat every request as independent and optimize for even distribution. llm-d inverts this: it deliberately creates affinity between requests that share prefixes, concentrating them on the same pods. Counterintuitive from a load-balancing perspective, but it's the right call for LLM inference — the compute savings from cache reuse far outweigh the marginal cost of slightly uneven load. The EPP balances both concerns, factoring in cache affinity and current pod load, and only routes to a cache-holding pod if it has enough capacity to serve the request without a lot of queuing delay.

Multi-Agent Scenario

Agent A (Research): web_search + knowledge_base + summarize (shared tools: 600 tokens) + unique system prompt (900 tokens)
Agent B (Support): web_search + knowledge_base + ticket_create (shared tools: 600 tokens) + unique system prompt (1,100 tokens)
Agent C (Code): web_search + code_execute + file_read (shared tools: 500 tokens) + unique system prompt (800 tokens)

Agents A and B share 600 tokens of tool definitions. All three share web_search (~200 tokens). llm-d can route requests from A and B to the same pod to exploit the 600-token shared prefix, while still routing C to that pod when it benefits from the 200-token web_search cache.

MCP and llm-d: How Model Context Protocol Workloads Flow Through the Routing Layer

The Model Context Protocol (MCP) is quickly becoming the standard way to connect LLM agents to external tools and data sources. MCP defines a structured format for exposing tools, resources, and prompts to LLM apps. And there's an underappreciated consequence for inference routing here: MCP creates predictable, standardized prefix patterns that llm-d can exploit for caching.

When an MCP-compliant agent starts up, it connects to one or more MCP servers and pulls their tool manifests. Those manifests get translated into the tool definition blocks that form part of the agent's prompt prefix. Because MCP defines a standard schema for tool definitions (name, description, input schema with JSON Schema types, annotations), the resulting token sequences are highly regular and predictable across different agents connecting to the same MCP servers.

MCP Creates a Natural Caching Hierarchy

Look at the layers of an MCP-enabled agent's prompt:

  1. Layer 1: MCP tool definitions, shared across all agents connecting to the same MCP servers. Highly cacheable.
  2. Layer 2: Agent system prompt, shared across all instances of the same agent type. Cacheable per agent type.
  3. Layer 3: Conversation history, unique per conversation session. Cacheable only with sticky routing.
  4. Layer 4: Current user message, unique per request. Never cached.

llm-d can exploit every layer of this. Requests from different agent types that share MCP servers get Layer 1 caching. Requests from the same agent type get Layers 1 and 2. Requests within the same conversation get Layers 1, 2, and 3. The deeper the shared prefix, the bigger the savings.

MCP Flow Through llm-d

Step 1: Agent connects to MCP servers (filesystem, database, web search) and retrieves tool manifests.
Step 2: Tool manifests are serialized into the prompt's tool definition block (~700 tokens).
Step 3: Agent's system prompt is prepended (~1,200 tokens).
Step 4: First inference request arrives at llm-d with full prefix (1,900 tokens).
Step 5: llm-d routes to a pod, which computes and caches the KV for all 1,900 tokens.
Step 6: Subsequent MCP tool calls from this agent (and others sharing the same MCP servers) route to the same pod, skipping up to 1,900 tokens of prefill.

MCP plus llm-d works out really well for tool-call-heavy workflows. Say an agent runs a multi-step task with five consecutive tool calls. Each one follows the same pattern: send a request to the LLM with the full context (system prompt + tool definitions + conversation so far + tool result), the LLM picks the next tool, repeat. Without cache-aware routing, each of those five requests recomputes the entire growing prefix from scratch. With llm-d's sticky routing, only the incrementally new tokens (the latest tool result and new user context) need fresh prefill.

MCP + llm-d fit together really well. If you're building agents without cache-aware routing, you're leaving 70% of your latency budget on the floor. Every MCP tool call that hits a cold cache is a call that makes your user wait for work the GPU already did.

MCP also has prompt templates: standardized prompt structures that MCP servers can expose for common tasks. When multiple agents use the same MCP prompt template, the token sequences share even more prefix overlap, which pushes llm-d's cache hit rates higher still. As MCP adoption grows and tool definitions get more standardized across the ecosystem, cache efficiency on llm-d-routed clusters should just keep improving.

One practical tip if you're the one running the platform. If you control the MCP servers your agents connect to, order your tool definitions consistently. llm-d matches prefixes from the start of the prompt, so putting the most commonly shared tool definitions first in the serialization order maximizes the shared prefix length across agents. Small detail, outsized impact on cache hit rates in multi-agent deployments.

Latency Analysis: How Much Time Intelligent Routing Saves Per Agent Execution

Okay, let's put real numbers on this. The analysis below models a realistic agentic workload and compares total Time-to-First-Token (TTFT) with and without llm-d's cache-aware routing.

Setup: A Typical Agent Execution

Without llm-d: Random Routing

With random or round-robin routing, each of the 8 calls lands on a different pod (or at best, a pod that already evicted any relevant cache). Every call pays full prefill for the shared prefix plus its unique context:

Call 1: (1500 + 800 + 150) / 5500 = 445ms
Call 2: (1500 + 800 + 150) / 5500 = 445ms
Call 3: (1500 + 800 + 200) / 5500 = 454ms  (growing context)
Call 4: (1500 + 800 + 250) / 5500 = 463ms
Call 5: (1500 + 800 + 300) / 5500 = 472ms
Call 6: (1500 + 800 + 350) / 5500 = 481ms
Call 7: (1500 + 800 + 400) / 5500 = 490ms
Call 8: (1500 + 800 + 450) / 5500 = 500ms

Average TTFT: ~469ms
Total prefill time across 8 calls: 3,750ms

With llm-d: Cache-Aware Routing

With llm-d, call 1 computes the full prefix and caches it. Calls 2 through 8 only compute attention for the new tokens (latest tool result, user message, or agent reasoning). Each call also picks up a small overhead (~40ms) for cache lookup and validation:

Call 1: (1500 + 800 + 150) / 5500 = 445ms  (cold start, full prefill)
Call 2: 150 / 5500 + 40ms overhead  =  67ms  (only new tokens)
Call 3: 200 / 5500 + 40ms overhead  =  76ms
Call 4: 250 / 5500 + 40ms overhead  =  85ms
Call 5: 300 / 5500 + 40ms overhead  =  95ms
Call 6: 350 / 5500 + 40ms overhead  = 104ms
Call 7: 400 / 5500 + 40ms overhead  = 113ms
Call 8: 450 / 5500 + 40ms overhead  = 122ms

Average TTFT (calls 2-8): ~95ms
Total prefill time across 8 calls: 1,107ms
3,750ms
Total prefill without llm-d
1,107ms
Total prefill with llm-d
70%
Reduction in prefill time
2.6s
Saved per agent execution

At Scale: 1,000 Concurrent Agents

Now multiply by the number of concurrent agents in a real deployment. A mid-sized platform might run 1,000 concurrent agent executions, each making 8 LLM calls. That's 8,000 inference requests in a burst. Without llm-d, those requests eat 3,750 seconds of cumulative prefill time (across all requests). With llm-d, it drops to 1,107 seconds — 2,643 seconds of GPU compute freed up to serve actual new requests instead of recomputing cached prefixes.

In GPU-hour terms: at typical A100 utilization, the prefill savings across 1,000 concurrent agents come out to roughly 44 GPU-minutes of freed compute per minute of wall-clock operation. Nearly 44 GPUs' worth of prefill work, just gone. At current cloud pricing for A100 instances, that can mean tens of thousands of dollars a month in reduced infrastructure spend for a large agentic platform. And the savings grow linearly with concurrent agents and calls per execution.

Latency Budget

Where the Time Goes in an Agent Execution

Without llm-d: Prefill (3.75s) + Generation (2.4s) + Tool execution (1.2s) + Network overhead (0.3s) = 7.65s total

With llm-d: Prefill (1.11s) + Generation (2.4s) + Tool execution (1.2s) + Network overhead (0.3s) = 5.01s total

Prefill goes from the dominant latency component (49% of total) to a smaller one (22% of total). The agent ends up bottlenecked by generation speed and tool execution time — which is where it should be bottlenecked, because that's actual useful work instead of redundant recomputation.

One more dimension that matters in practice: tail latency. Without cache-aware routing, every call has roughly the same TTFT distribution because every call does roughly the same work. With llm-d, calls 2 through N have dramatically lower and more consistent TTFT, because cached prefill is a deterministic operation (cache lookup) rather than a variable one (GPU-dependent prefill computation). So llm-d doesn't just improve median latency — it tightens the p99, which is often the number that actually matters for user-facing agents.

Cost Impact: What Cache-Aware Routing Saves in Real Dollars

The latency wins translate straight into infrastructure cost savings. Freed GPU compute means you either serve more traffic on the same hardware or run the same traffic on fewer GPUs. Here's the math for a concrete deployment.

Cost Analysis

Monthly Savings for a Mid-Scale Agentic Platform

Cluster configuration
8x A100 80GB GPUs
Hourly cost per GPU (cloud)
$2.21/hr
Monthly GPU spend (24/7)
$12,730/mo
Prefill savings with llm-d
~$4,200/mo

At moderate agent traffic (500 to 1,000 concurrent agents during peak hours), cache-aware routing kills roughly 70% of redundant prefill compute. For a cluster running 8x A100 GPUs at $2.21/hr each, the freed capacity works out to about $4,200/month in savings — either serve the same traffic on fewer GPUs, or absorb 33% more traffic on the existing cluster without buying more hardware. At higher traffic or with larger models (which have proportionally higher prefill costs), the savings scale up with you.

And honestly, these numbers are conservative. They only count prefill compute savings. You also get fewer autoscaling events (fewer cold-start pods), lower network overhead (sticky routing cuts cross-zone traffic), and better GPU utilization (less memory wasted on duplicate KV caches) — all of which push the total cost impact higher.

The Agentic Scaling Challenge: Why Agent Workloads Are Burstier and Harder to Predict

Agentic workloads don't just add more inference requests — they change the shape of traffic. Worth understanding if you're building infrastructure to serve agents at scale.

The Amplification Problem

In a traditional LLM app, one user request produces one inference call. Traffic is fairly predictable: 100 users averaging 2 requests per minute means roughly 200 inference calls per minute. You can provision and autoscale for that with standard techniques.

Agents break this completely. One user request might produce 3 inference calls (a simple lookup) or 25 (a complex multi-step research task). The amplification factor is variable and data-dependent — it depends on the agent's reasoning, the complexity of the request, and what the intermediate tool calls return. You can't predict it from the input alone. An agent that usually makes 5 calls might make 20 if a tool errors out and the agent decides to try alternatives.

1:N
User request to inference call ratio
3-25x
Typical amplification range
5x
Parallel burst factor

Burst Patterns in Agentic Workloads

It gets worse: agents often generate parallel bursts of inference requests. A research agent that decides to search five sources at once fires off five inference calls simultaneously. A code agent that needs to analyze three files before picking a fix might kick off three parallel analysis calls. These bursts are sudden, short-lived, and intense.

Bursts also correlate in time. If your platform serves 100 concurrent users and each one triggers an agent, those 100 agents don't neatly stagger their calls. They cluster — lots of agents hit their "tool-calling" phase at the same time, and the parallel bursts stack. What you get looks less like a steady stream and more like sharp spikes separated by relative calm.

Traditional autoscaling just can't keep up with this. Kubernetes HPA (Horizontal Pod Autoscaler) typically works on a 15 to 60 second observation window, and spinning up a new vLLM pod takes 30 to 120 seconds (model loading alone can take 30+ seconds on large models, before GPU memory is even allocated and weights transferred). By the time the autoscaler notices the burst and provisions capacity, the burst is over. The new pods sit idle, costing money, while the next burst slams the existing pods that are still recovering from the last one.

How llm-d's Load-Aware Routing Helps Absorb Bursts

llm-d attacks the burst problem with load-aware routing that works at the request level, not the pod level. When a burst arrives, the EPP checks current queue depth and active request count on every pod in real time, then routes requests to pods with available capacity — spreading the burst across the cluster without waiting on autoscaling to provision anything.

The important bit: cache-awareness and load-awareness work together as one cost function, not competing priorities. The EPP doesn't blindly route every cache-sharing request to the same pod if that pod is slammed. It runs a dynamic cost model that weighs the compute savings of cache reuse against the queuing delay of routing to a busy pod. If the cache-holding pod's queue depth crosses a configurable threshold, the EPP routes the request to a less-loaded pod and takes the cache miss — the queuing delay would've cost more than the recomputation anyway.

Design Principle

Cache Affinity Is Not Stickiness at All Costs

A naive cache-aware router would always send requests to the pod with the cached prefix, even with 50 requests queued there. llm-d models the actual trade-off — cache recomputation time (hundreds of milliseconds) vs queuing delay (potentially seconds) — and makes a dynamic decision per request. Lightly loaded cluster: cache affinity dominates. Heavily loaded: load balancing dominates. The EPP shifts smoothly between the two based on real-time cluster state.

This dynamic balancing matters most during bursts. When five parallel agent calls arrive at once, the first one or two might route to the cache-holding pod. But if calls three through five would queue behind them, llm-d sends them to other pods with capacity. Those calls pay more prefill, but they start executing immediately instead of sitting in a queue. Net result: lower end-to-end latency for the burst, even though individual cache hit rates drop.

llm-d also exposes metrics that external autoscalers can actually use. Per-pod queue depths, cache utilization ratios, request arrival rates — a much richer picture of cluster state than plain CPU or memory utilization. A custom autoscaler wired into llm-d's metrics can anticipate capacity needs from active agent sessions and their historical amplification factors, instead of reacting to pod resource utilization after the fact. For bursty agent traffic, predictive scaling like that beats reactive HPA rules by a lot.

Agentic Routing Challenges: Fan-Out, Cascading Latency, and Retry Storms

The routing layer for agentic workloads runs into three specific problems that don't exist (or are way milder) in single-call LLM apps. Worth knowing about all three before your agents get complex enough to hit them.

Fan-Out Patterns: The Thundering Herd Problem

A lot of agent architectures do fan-out: a single user request triggers N parallel LLM calls. Research and analysis agents do this constantly. Ask one to "summarize recent developments in quantum computing" and it might fan out five parallel searches — arxiv papers, industry news, GitHub activity, patent filings, conference proceedings. Each search generates its own LLM call to interpret and summarize results. All five run concurrently, so what looks like a single request to the user is a sudden 5x load spike at the inference layer.

That fan-out creates a thundering herd problem. If 100 users simultaneously trigger agents that each fan out to 5 parallel calls, the cluster suddenly faces 500 concurrent requests instead of 100. Traditional load balancers spread those 500 requests evenly across pods, which sounds reasonable — until you think about the KV cache. Without cache-aware routing, all 500 requests land on random pods. Even when requests share prefixes (common tool definitions, shared system prompts), the random distribution scatters those prefixes across different pods and kills any chance of cache reuse.

Fan-Out Scenario

User request: "Analyze this pull request for security issues"
Agent fan-out:
- Call 1: Scan for SQL injection patterns (parallel)
- Call 2: Check for XSS vulnerabilities (parallel)
- Call 3: Analyze authentication logic (parallel)
- Call 4: Review cryptographic implementations (parallel)
- Call 5: Check dependency versions against CVE database (parallel)

All five calls share the same system prompt (1,200 tokens) and tool definitions (700 tokens). Without llm-d, each call recomputes 1,900 tokens of shared prefix. With llm-d's cache-aware routing, all five calls route to pods that have the shared prefix cached, saving 7,600 tokens of redundant prefill across the fan-out.

llm-d handles fan-out by noticing when multiple requests from the same agent session arrive within a short window (typically 50ms to 200ms) and routing them to a pod cluster that can share the cached prefix. The EPP tracks not just individual request prefixes but session-level prefix groups. When it detects a fan-out (same session ID, similar timestamps), it routes the requests to pods in the same availability zone or pod group, maximizing the odds they land on pods sharing the same prefix cache.

The scale impact adds up fast. In a deployment with 1,000 concurrent users where 20% trigger fan-outs averaging 5 parallel calls each, the instantaneous request load isn't 1,000 — it's 1,800 (800 single-call requests plus 1,000 fan-out calls from 200 users). Cache-aware routing lets those 1,000 fan-out calls share prefix caching, cutting the effective prefill load from 1,800 full prefills to roughly 1,000 (800 singles + 200 initial fan-out calls that cache the prefix). That's a 44% reduction in prefill compute during peak fan-out periods.

Cascading Latency: The Chain Is Only as Fast as Its Slowest Call

Fan-out gives you parallel load spikes, but plenty of agent workflows are the opposite: sequential dependencies, where one LLM call can't start until the previous one finishes. That's a cascading latency problem — total execution time is the sum of all sequential steps, and one slow call adds hundreds of milliseconds to the whole run.

Take a customer support agent handling a refund. The chain: (1) parse user intent (100ms generation), (2) look up order details via tool call (50ms), (3) LLM interprets order data and decides next step (120ms generation), (4) check refund eligibility via tool call (80ms), (5) LLM determines refund amount and policy (110ms generation), (6) initiate refund via tool call (200ms), (7) LLM confirms success and composes response (140ms generation). Seven steps, each depending on the one before.

Now add inference latency. Each of those four LLM calls (steps 1, 3, 5, 7) needs a prefill phase before generation starts. Without cache-aware routing, each call hits a random pod and pays full prefill: roughly 400ms per call for a 2,300-token prefix. Total: (400+100) + 50 + (400+120) + 80 + (400+110) + 200 + (400+140) = 2,400ms of LLM time plus 330ms of tool time = 2,730ms total.

With llm-d, the first call pays the 400ms prefill, but calls 2, 3, and 4 only pay 60ms to 80ms for their unique context (the prefix is cached). Total: (400+100) + 50 + (70+120) + 80 + (65+110) + 200 + (60+140) = 1,395ms of LLM time plus 330ms of tool time = 1,725ms total. A 1,005ms (37%) cut in end-to-end latency, purely from not redoing prefill on sequential calls.

Latency Amplification

Why Cache Misses Hurt More in Sequential Chains

In parallel fan-out, a cache miss on one call doesn't delay the others — they run independently. In sequential chains, every millisecond added at step N delays everything after it. One cache miss that adds 350ms to step 3 delays steps 4, 5, 6, and 7 by the same amount. The user doesn't just eat the 350ms — they feel the whole longer wait. This is why cache hit rates matter more for sequential agent workflows than parallel ones.

llm-d's sticky routing exists precisely to stop these cascading penalties. Keeping an entire agent session pinned to the same pod (or pod group) means every call in a sequential chain rides the cached prefix computed on call one. The EPP tracks session IDs and holds pod affinity for the life of the session, typically via a TTL-based cache entry that expires after the session ends or after a configurable idle timeout (usually 5 to 15 minutes).

Retry Storms: When Failures Multiply Inference Load

Agents are brittle. Tool calls fail — timeouts, rate limits, transient network errors, malformed responses. When a tool call fails, most agent frameworks retry with a modified prompt, often adding context about the failure ("The previous call to search_database timed out. Try a more specific query"). That retry behavior can spiral into exponential retry storms where each failure generates more LLM calls, potentially 2x to 3x the original volume.

Concrete example. An agent calls a search tool with a broad query. It times out. The agent retries with a narrower query — also times out. So the agent splits the search into three parallel narrower queries (fan-out). Two succeed, one fails. The agent retries the failed one with an even more specific version. What started as one LLM call is now six (original + first retry + three parallel retries + one final retry). If 10% of agent requests hit this pattern, your cluster sees a 15% to 20% load increase from retries alone.

The interaction with routing is subtle but it matters. In a naive routing system, each retry lands on a random pod and recomputes the whole prefix from scratch. So a retry storm doesn't just bump call volume — it bumps prefill compute, because every retry is a cache miss. In the six-call example, that's six full prefills of the 2,300-token prefix: 2,400ms of redundant compute for what should have been one prefill plus five incremental updates.

Retry storms feed themselves. Retries increase cluster load. More load causes more timeouts and rate limits. More timeouts trigger more retries. Without smart routing and backoff policies, this feedback loop can degrade the whole cluster, with retry traffic crowding out new requests. llm-d's cache-aware routing doesn't prevent retries, but it makes sure they don't pay the full prefill penalty, which takes a lot of the sting out.

Cache-aware routing contains retry storms in two ways. First, retries from the same agent session share the original call's prefix. If the original cached the prefix on pod A, llm-d routes the retry to pod A, where it's already in cache. The retry only pays to prefill the incremental context (the error message and modified query) — typically 50 to 150 tokens instead of 2,300. That cuts per-retry prefill cost by 85% to 95%.

Second, the EPP can do retry-aware load shedding. When a pod's queue depth crosses a critical threshold (overload territory), the EPP can tell the agent runtime to delay or abort retries, via HTTP 429 (Too Many Requests) responses with a Retry-After header. Agents that respect the signal back off, and the storm never overwhelms the cluster. It does require coordination between the routing layer (llm-d) and the agent runtime, but you end up with a system that degrades gracefully under retry load instead of falling over.

The pattern that works in practice: combine cache-aware routing (cheap retries when they do happen) with exponential backoff and jitter (spread retry load over time) and circuit breaking (fail fast when a tool is systematically down). llm-d handles the routing piece; the agent framework has to handle backoff and circuit breaking. Get all three working together and retry storms become an operational annoyance instead of a catastrophic failure mode.

Observability for Agents: Instrumenting Multi-Step Workflows with OpenTelemetry

Agentic workloads are opaque. A user submits a request and waits seconds for a response — but what happened during those seconds? Which LLM calls were made? Which tools ran? Where did the latency come from? Without instrumentation, debugging performance issues in a live agent is nearly impossible. This is where observability earns its keep, and OpenTelemetry (OTel) has become the standard for distributed tracing in agentic systems.

Instrumenting Multi-Step Agent Workflows with OpenTelemetry

OpenTelemetry gives you a vendor-neutral way to instrument code with traces (the flow of a request through a system) and spans (individual operations within that flow). For agents, the natural mapping: one trace per agent execution, one span per step — each LLM call, each tool invocation, each routing decision.

Here's what that looks like in practice. When an agent runtime gets a user request, it creates a parent span for the whole agent execution. Each LLM call gets a child span. Each tool invocation gets a child span. You end up with a hierarchical trace showing the complete flow of the agent's reasoning, including time spent in each phase.

import { trace } from '@opentelemetry/api';

async function executeAgent(userRequest) {
  const tracer = trace.getTracer('agent-runtime');
  const parentSpan = tracer.startSpan('agent.execute', {
    attributes: {
      'agent.type': 'customer-support',
      'user.request': userRequest
    }
  });

  try {
    // Step 1: Parse user intent
    const intentSpan = tracer.startSpan('agent.llm.call', {
      attributes: { 'llm.step': 'parse-intent' }
    });
    const intent = await callLLM(promptForIntent(userRequest));
    intentSpan.end();

    // Step 2: Tool call to look up order
    const toolSpan = tracer.startSpan('agent.tool.call', {
      attributes: { 'tool.name': 'lookup-order' }
    });
    const order = await lookupOrder(intent.orderId);
    toolSpan.end();

    // Step 3: LLM interprets order data
    const interpretSpan = tracer.startSpan('agent.llm.call', {
      attributes: { 'llm.step': 'interpret-order' }
    });
    const decision = await callLLM(promptForDecision(order));
    interpretSpan.end();

    // ... more steps ...

    parentSpan.setStatus({ code: SpanStatusCode.OK });
  } catch (error) {
    parentSpan.setStatus({
      code: SpanStatusCode.ERROR,
      message: error.message
    });
  } finally {
    parentSpan.end();
  }
}

That gives you a trace with a parent span and a bunch of child spans. Each span carries attributes (metadata) describing what operation it was, what parameters it used, and whether it succeeded. Export the trace to an observability backend (Jaeger, Tempo, Honeycomb) and you can see the entire agent execution as a waterfall — which steps ran sequentially, which ran in parallel, and where the time went.

Correlating Traces Across Agent Steps: Propagating Context Through the Routing Layer

Where OTel really pays off for agents is trace context propagation across service boundaries. When an agent runtime makes an LLM call to llm-d, which routes it to a vLLM backend, the trace context needs to survive all three hops (agent → llm-d → vLLM) so one Jaeger trace shows the complete flow, routing decisions included.

This happens via the traceparent HTTP header, part of the W3C Trace Context spec. The agent runtime includes a traceparent header (trace ID + parent span ID) on its inference request to llm-d. llm-d's routing layer extracts it, creates its own child span for the routing decision, and forwards the header (updated with its own span ID) to the vLLM backend, which creates a child span for the inference computation. Result: a three-level trace — agent execution → routing decision → inference computation.

Trace Context Flow

Step 1: Agent runtime creates parent span with trace ID abc123 and span ID span-001.
Step 2: Agent makes HTTP request to llm-d with header traceparent: 00-abc123-span-001-01.
Step 3: llm-d extracts trace context, creates child span span-002 for routing decision.
Step 4: llm-d routes to vLLM pod with header traceparent: 00-abc123-span-002-01.
Step 5: vLLM creates child span span-003 for inference, exports trace to Jaeger.

In Jaeger, the trace appears as a single waterfall with three spans: agent execution (5.2s), routing decision (2ms), inference (1.1s). You can see that routing added negligible overhead and that inference accounted for 21% of the total latency.

llm-d preserves trace context with OpenTelemetry-aware middleware. When a request hits the EPP, the middleware extracts the traceparent header, creates a child span for the routing decision (with attributes like routing.strategy: cache-aware, routing.cache-hit: true, routing.pod: vllm-pod-3), and injects the updated trace context into the outbound request to vLLM. Happens transparently on every request, typically under 1ms of overhead.

The payoff is real. When an agent execution is slow, open the trace in Jaeger and you immediately see the bottleneck. Prefill? Cache miss. Generation? The model is grinding on the prompt. Tool call? Some external API is slow. Queuing delay at the routing layer? Cluster overload. It's all right there in the trace, no manual log correlation required.

Measuring End-to-End Agent Latency vs Individual Call Latency

One of the most common observability mistakes in agentic systems: conflating per-call TTFT (what vLLM reports) with end-to-end agent latency (what the user actually experiences). Different metrics, and the gap between them grows with agent complexity.

Per-call TTFT is the time from vLLM receiving a request to emitting the first token. It's the metric vLLM exposes on its /metrics endpoint and what most LLM serving benchmarks measure. For a single call, it's a decent proxy for user-perceived latency. For agent workflows, it isn't. An agent making eight LLM calls at an average TTFT of 150ms can still have 5 seconds of end-to-end latency, because the eight calls run sequentially with tool execution and network overhead in between.

150ms
Average per-call TTFT
1,200ms
Total LLM time (8 calls)
5,000ms
End-to-end agent latency
3,800ms
Non-LLM overhead

The gap comes from sequential dependencies, tool execution time, and network round trips. Each LLM call waits on the previous tool call. Each tool call adds 50ms to 500ms depending on the external API. Each network round trip (agent → llm-d → vLLM → llm-d → agent) adds 5ms to 20ms depending on topology. It all compounds across the call chain.

The metrics I'd actually track for agent observability:

Track these over time and you catch trends per-call TTFT can't show you. Cache hit rate drops from 85% to 60%? End-to-end agent latency climbs even though per-call TTFT looks flat. External API degrades and tool time creeps up? End-to-end latency suffers even though LLM performance is unchanged. OTel's structured traces and span attributes let you compute all of this automatically from exported trace data.

Observability Best Practice

Set SLOs on End-to-End Latency, Not Per-Call TTFT

Lots of teams set performance SLOs on vLLM's reported TTFT because that's the metric the serving layer hands you. For agents, that's a mistake. The user doesn't care that each individual LLM call finishes in 120ms if the overall agent takes 6 seconds to respond. Set SLOs on agent execution time (p50, p95, p99) and treat per-call TTFT as a diagnostic for why agent latency is high. OpenTelemetry traces give you the breakdown; the SLO should target what the user feels.

Agentic Architecture Patterns: How llm-d Optimizes Common Agent Designs

Agentic AI has settled on a handful of common architectural patterns, and it's worth knowing how llm-d handles each one. The four below cover the majority of deployed agent systems.

ReAct Loops: Reason + Act Iterations

ReAct (Reasoning and Acting) is probably the most widely used agent architecture. The agent alternates between reasoning steps (deciding what to do next) and action steps (executing a tool call based on that reasoning), looping until it has enough information to produce a final answer.

A typical ReAct run: (1) the agent gets a user query, (2) generates a reasoning step explaining what it needs, (3) calls a tool to get it, (4) observes the result and reasons again based on what it learned, (5) either calls another tool or produces a final answer. Each iteration builds on the last, and the conversation history grows with every reasoning and action step.

ReAct Loop Example

User: "What is the current stock price of Tesla and how has it changed in the past month?"

Iteration 1:
- Reasoning: "I need to look up Tesla's current stock price."
- Action: get_stock_price("TSLA")
- Observation: "$187.32 as of 2:47pm EST"

Iteration 2:
- Reasoning: "I have the current price. Now I need historical data for the past month."
- Action: get_stock_history("TSLA", days=30)
- Observation: "30 days ago: $202.15. Change: -7.3%"

Iteration 3:
- Reasoning: "I have both data points. I can now answer the user."
- Final Answer: "Tesla's current stock price is $187.32. Over the past month, it has decreased by 7.3% from $202.15."

The routing challenge: each iteration shares a growing prefix with the previous one. Iteration 1 starts with the system prompt (1,500 tokens) plus tool definitions (800 tokens) plus the user query (50 tokens) = 2,350 tokens. Iteration 2 adds iteration 1's reasoning (60 tokens) plus the tool result (20 tokens) = 2,430 tokens of prefix. Iteration 3 adds iteration 2's reasoning (70 tokens) plus the second tool result (40 tokens) = 2,540 tokens of prefix.

Without llm-d, each iteration lands on a random pod and recomputes the whole growing prefix from scratch. Iteration 1 pays 427ms for prefill. Iteration 2 pays 442ms. Iteration 3 pays 462ms. Total: 1,331ms. With llm-d's sticky routing, iteration 1 pays 427ms, but iterations 2 and 3 only pay for the tokens added since the last iteration — about 15ms for iteration 2 and 20ms for iteration 3. Total: 462ms. A 65% reduction.

ReAct Optimization

Why Sticky Routing Is Essential for ReAct

ReAct loops run 3 to 15 iterations depending on task complexity. Each iteration builds on the growing conversation history, so the prefix gets longer every loop. Without sticky routing, prefill cost per iteration climbs linearly as the loop goes. With it, only the incremental tokens (new reasoning and tool result) get prefilled each iteration. The longer the loop, the bigger the win.

llm-d's session-aware routing keeps the whole ReAct loop pinned to one pod. The first iteration caches the initial prefix (system prompt + tools + user query); every iteration after reuses that cache and only prefills the new reasoning and observation tokens. For a 10-iteration loop, that can take total prefill from over 4 seconds to under 600ms — an 85% cut that shows up directly in agent response time.

Tool-Augmented Generation (TAG): Pausing Mid-Generation for Tool Calls

Tool-Augmented Generation (TAG) is the pattern where the agent generates text, pauses to call a tool, folds the tool result into its context, and keeps generating. Unlike ReAct (discrete reasoning/action steps), TAG interleaves tool calls inside a single generation stream.

A TAG run might go: the agent starts generating ("Based on recent data..."), realizes mid-sentence it needs a specific data point, pauses, calls a tool, then continues with the result in context. The key difference from ReAct: it doesn't finish a full reasoning step before calling a tool — it calls the moment it realizes it needs to, even mid-sentence.

The inference challenge: each tool call breaks the generation stream. The agent generates 50 tokens, pauses for a tool call, then resumes. From the LLM's perspective that's two separate inference requests — one for the initial 50 tokens, one for the continuation after the tool result. The second request's prefix is the first request's full context plus the tool result.

Request 1 (initial generation):
  System prompt (1500 tok) + Tools (800 tok) + User query (40 tok)
  → Generate 50 tokens of response
  → Agent pauses, calls tool

Tool result returns (30 tokens of data)

Request 2 (continuation):
  System prompt (1500 tok) + Tools (800 tok) + User query (40 tok)
  + Generated so far (50 tok) + Tool result (30 tok)
  → Generate final 80 tokens of response

Without llm-d, request 2 lands on a random pod and recomputes the full 2,420-token prefix (system + tools + query + first 50 generated tokens + tool result). That's 440ms of prefill for a request that only adds 30 new tokens of context. With llm-d, request 2 goes back to the pod that handled request 1, where the KV cache for the first 2,340 tokens is still resident. Only the 80 new tokens (50 generated + 30 tool result) need prefill — about 15ms.

TAG is where cache-aware routing gets its biggest per-call wins. Because the agent resumes generation right after a tool call, the previous request's prefix is still warm in the cache. Hit rates for TAG continuations can top 95% when routed properly. That's why TAG-style agents feel noticeably snappier than ReAct-style agents with the same number of tool calls.

llm-d makes sure TAG continuations hit the cache by tracking request sequences within a session. When a request comes in with a session ID matching a recent request (within the last 5 to 30 seconds), the EPP routes it to the pod that handled the previous one, assuming that pod still holds the session's KV cache. This works even when the agent fires multiple parallel tool calls and then resumes — llm-d routes the resumption to the pod with the largest shared prefix, which is almost always the pod that handled the pre-pause generation.

Multi-Agent Orchestration: Supervisor Delegates to Worker Agents

In multi-agent orchestration, a supervisor agent delegates to specialized workers. The supervisor gets a user request, breaks it into subtasks, dispatches each to the right worker (research worker, code worker, analysis worker), then aggregates the results into a final response. Each worker has its own system prompt and tooling, but they may share common tool definitions (logging, notification, knowledge base access).

The routing angle: supervisor and workers all make LLM calls, and those calls may share partial prefixes. If everyone shares the same 5 common tools (600 tokens of tool definitions), cache-aware routing can exploit that shared prefix even though the system prompts differ.

Multi-Agent Orchestration

User request: "Prepare a competitive analysis report for our Q3 product launch."

Supervisor agent:
- Breaks request into: market research, competitor feature comparison, pricing analysis
- Dispatches three subtasks to worker agents in parallel

Worker 1 (Research): Searches news, reports, and market data
Worker 2 (Features): Analyzes competitor product pages and documentation
Worker 3 (Pricing): Scrapes pricing pages and compares tiers

Supervisor agent:
- Receives results from all three workers
- Synthesizes into a final report and returns to user

Each worker might make 3 to 8 LLM calls. The supervisor makes 2 (initial task decomposition and final synthesis). That's 11 to 26 LLM calls for a single user request. Without cache-aware routing, each one lands on a random pod. With llm-d, supervisor and workers route to pods that can share the common tool definitions, and each worker's internal call sequence gets sticky routing on top.

llm-d's prefix matching gives multi-agent orchestration cross-agent cache sharing. When worker 1 and worker 2 both start, llm-d sees they share 600 tokens of common tool definitions and routes them to pods that have those cached. Worker 1's first call might cache the prefix on pod A. Worker 2's first call routes to pod A (or another pod in the same group) and reuses the cached tool definitions, saving 600 tokens of prefill. Even the supervisor's final synthesis call routes to a pod with the shared tool definitions cached, despite having a different system prompt from the workers.

At scale this adds up quickly. With 500 concurrent multi-agent requests (each spawning 3 workers), you've got 2,000 active agent sessions at any moment. Traditional routing spreads those across all available pods, fragmenting the cache. llm-d's cross-agent cache sharing concentrates agents with similar tool definitions onto the same pods, pushing cache hit rates from 20-30% (random routing) to 70-85% (cache-aware). At this scale, that's the difference between needing 20 pods and needing 12 for the same traffic.

Supervisor/Worker Patterns: Planning Agent Creates Subtasks for Specialist Agents

The supervisor/worker pattern is a specialization of multi-agent orchestration: the supervisor is a planning agent that builds a detailed execution plan with subtasks, and workers are specialists that execute them independently. The difference from generic orchestration is the hierarchy — the supervisor's system prompt is often a superset of the workers' prompts, or the workers' prompts come from templates that share structure with the supervisor's.

That hierarchy opens up prefix nesting. If the supervisor's system prompt is 2,000 tokens and includes a section defining the overall task domain, and each worker's system prompt is 1,200 tokens with the same domain definition (400 tokens) plus worker-specific instructions (800 tokens), then supervisor and workers share a 400-token prefix. llm-d's partial prefix matching can exploit that.

Hierarchical Caching

Supervisor Prompts as Cache Seeds for Workers

When the supervisor's system prompt is a superset of the workers', the supervisor's first LLM call caches the full prompt, shared domain definition included. When workers start, their prompts share the first N tokens with the supervisor's cached prompt, so llm-d routes them to pods holding that prefix and they reuse the shared portion of the KV cache. Works especially well when the supervisor runs on a dedicated pod and workers fan out to a pod group that shares cache state with it.

Concrete example: a supervisor agent gets "Build a web scraper for product data." Its system prompt defines the task domain ("You are building data extraction tools for e-commerce...") plus planning instructions. It creates subtasks: (1) design scraper architecture, (2) implement HTML parser, (3) implement rate limiting, (4) write tests, and dispatches each to a worker. The workers' system prompts start with the same domain definition (400 tokens) and then add worker-specific instructions.

Without llm-d, supervisor and workers land on random pods, and the domain definition (400 tokens) gets computed 5 times — once for the supervisor, once per worker. With llm-d, the supervisor's first call caches it, and the 4 workers route to pods with that prefix cached, saving 1,600 tokens of redundant prefill (400 tokens × 4 workers). If each worker makes 3 LLM calls, that's 12 calls riding the shared prefix cache — 4,800 tokens of prefill saved.

The pattern that works in practice: tag supervisor and worker requests with metadata about their relationship (supervisor ID, worker ID, task ID). The EPP uses it to route workers to pods that handled the supervisor's initial planning call, maximizing prefix sharing. Your agent framework has to put the metadata in the request headers, but the routing savings easily justify that small integration cost.

Pattern Comparison at a Glance

The four patterns above each behave differently in terms of LLM call volume, cache behavior, and how much llm-d's routing helps them. Here's a side-by-side to help you figure out which fits your workload.

ReAct Loop
Alternates between reasoning and tool-calling steps in a sequential loop. The conversation history grows with each iteration, creating increasingly long shared prefixes.
LLM Calls
3 - 15
Call Pattern
Sequential
Prefix Growth
Linear
Prefill Savings
65 - 85%
Best for: Iterative research, step-by-step problem solving
Tool-Augmented Generation
Pauses mid-generation to call tools, then resumes from the same context. Creates very short gaps between requests, so the cache is almost always warm.
LLM Calls
2 - 8
Call Pattern
Sequential
Prefix Growth
Incremental
Prefill Savings
90 - 97%
Best for: Interactive assistants, real-time data lookups
Multi-Agent Orchestration
A supervisor dispatches subtasks to specialized workers in parallel. Workers share partial prefixes (common tools) and each runs its own sequential call chain.
LLM Calls
11 - 26+
Call Pattern
Mixed
Prefix Growth
Branching
Prefill Savings
50 - 70%
Best for: Complex tasks requiring multiple specializations
🔍
RAG (Retrieval-Augmented)
Retrieves documents from a knowledge base and includes them in the prompt as context. The system prompt and retrieval instructions form a stable, cacheable prefix.
LLM Calls
1 - 3
Call Pattern
Sequential
Prefix Growth
Fixed
Prefill Savings
30 - 50%
Best for: Knowledge Q&A, document-grounded answers

Which Pattern Should I Use?

Picking an architecture comes down to task complexity, latency requirements, and how your agent talks to external tools. The decision tree below is a decent starting point.

Agentic Pattern Decision Tree
Does your task require calling external tools or APIs?
No
RAG Pattern
Retrieve relevant documents and include them as context. Low call count, stable prefix caching. Best for knowledge Q&A and search.
Yes
Does it need multiple specialized sub-agents?
Yes
Multi-Agent
Supervisor delegates to worker agents. Best for complex tasks needing research, code, and analysis in parallel.
No
Should tool calls interrupt mid-generation?
Yes
TAG Pattern
Pause generation, call tool, resume. Highest cache hit rates (95%+). Best for interactive, real-time experiences.
No
ReAct Loop
Think, then act, then observe. Iterative and predictable. Best for multi-step reasoning with clear tool boundaries.

These patterns aren't mutually exclusive. Real systems mix them all the time — a multi-agent orchestrator running ReAct loops inside each worker, or a RAG pipeline feeding a TAG-style interactive response. llm-d optimizes each pattern individually, and the benefits compound when you nest them.

Production Patterns for Agentic Inference

Running llm-d for agent workloads in production takes more than flipping on cache-aware routing. The patterns below are the kind of stuff you usually learn the hard way, and they apply whether you're serving dozens of concurrent agents or thousands.

Connection Pooling for Agent-to-Inference Communication

Agents are chatty. An agent making 8+ inference calls per execution shouldn't be opening and closing HTTP connections for each one. You want connection pooling between the agent runtime and the llm-d ingress — without it, TCP handshake and TLS negotiation add 5 to 20ms per call, and that compounds across the call chain just like prefill latency does.

The pattern: keep a pool of persistent HTTP/2 connections between each agent runtime pod and the llm-d routing layer. HTTP/2 multiplexing lets multiple inference requests fly concurrently over one connection, which matters a lot for agents doing parallel tool calls. Size the pool from the max concurrent inference requests per agent runtime pod — typically 10 to 50 connections for a pod running 20 to 100 concurrent agent executions. Watch connection utilization to right-size it: too few and you get head-of-line blocking on parallel bursts; too many and you're wasting kernel resources on idle sockets.

Request Coalescing: Batching Shared-Prefix Calls

When multiple agents share a prefix and fire inference requests at roughly the same time, llm-d can coalesce them into a single prefill computation. Instead of computing the shared prefix's KV cache once per request, the router spots matching prefixes within a short window (typically 10 to 50ms) and batches their prefill on the same pod. Each request still gets its own completion, but the shared prefix gets computed once and the KV cache is shared across the whole batch.

Coalescing shines on multi-tenant platforms where lots of agents share tool definitions. If 20 requests with the same 800-token tool definition prefix arrive within a 30ms window, coalescing cuts their prefill compute by 95% — prefix computed once, and the 20 unique suffixes batched for efficient GPU utilization. Conceptually it's like vLLM's continuous batching at the generation level, extended by llm-d to the routing and prefill layers.

Priority Queues: Differentiating Interactive and Background Calls

Not every call in an agent execution is equally urgent. The final response to the user is latency-sensitive — someone's actively waiting on it. Intermediate tool-call decisions, background reasoning steps, and self-verification checks, less so. Tag your inference requests with priority levels and let llm-d route accordingly.

Priority Queue Strategy

P0 (Critical): Final user-facing response generation. Route to lowest-queue-depth pod, even if it means a cache miss. Target TTFT: <100ms.
P1 (Interactive): Tool-call decision steps where the user is actively waiting. Prefer cache-aware routing but accept load-based fallback. Target TTFT: <200ms.
P2 (Background): Self-verification, logging summaries, pre-emptive caching. Route to cache-holding pod regardless of queue depth. Target TTFT: <2s acceptable.

The EPP can fold request priority into its routing cost function. High-priority requests weight queue depth more heavily (waiting is unacceptable), low-priority requests weight cache affinity more heavily (recomputation is wasteful when nobody's waiting). That way the cluster serves latency-sensitive interactive requests fast while still squeezing max cache utilization out of background work that can tolerate the wait.

Circuit Breaking: Handling Cache Thrashing

Cache thrashing happens when a pod's KV cache keeps getting evicted and recomputed under memory pressure from too many concurrent sessions. It's a nasty feedback loop: llm-d routes requests to the pod because it supposedly "has" the cache, but the cache got evicted before the request lands, so the request pays full prefill plus the queuing delay of being pinned to a specific pod instead of the least-loaded one. Worst of both worlds — cache-miss latency with affinity-routing overhead.

The fix is circuit breaking at the cache level. llm-d watches each pod's cache hit rate in real time. When a pod drops below a configurable threshold (say, under 40% over the last 60 seconds), the EPP marks it "cache-degraded" and stops sending cache-affinity requests its way, falling back to pure load-based routing until the hit rate recovers past a recovery threshold (say, 60%). That breaks the feedback loop and makes sure cache-aware routing only kicks in when it's actually paying off.

This does require the vLLM backends to expose cache statistics to the EPP — cache hits vs misses over a rolling window. llm-d's health-check protocol includes those metrics, so the EPP can make circuit-breaking decisions without adding latency to the hot path.

Graceful Degradation: When All Pods Are Cold

Sometimes cache-aware routing has nothing to work with: cluster cold starts after a deploy, scaling events bringing up new pods with empty caches, traffic spikes that blow past the cluster's total cache capacity. In those cases llm-d needs to fall back to pure load-based routing without adding cache-lookup overhead on top.

The EPP has a fast-path check for this: if no pod reports a cache entry matching the request's prefix hash, it skips the cache-aware routing logic entirely and routes on load metrics alone. No wasted time hunting for a cache match that doesn't exist — "all-cold" routing performs like a standard load balancer. As pods warm up and cache entries accumulate, the EPP shifts back to cache-aware routing on its own, no manual intervention or config change needed.

Same story at the prefix level. Deploy a new agent type with a system prompt no pod has cached, and llm-d treats its first request as a load-balanced cold start, building cache affinity from there. Within seconds the new prefix is cached and subsequent requests get cache-aware routing. It's automatic and invisible to the agent application — this "warm on first use" behavior means new agent types and system prompt updates need zero cache pre-warming. llm-d just handles it as part of normal request processing.


Conclusion: The Infrastructure Agentic AI Demands

Agentic workloads aren't some future thing — they're here now. Every major AI platform is shipping agent capabilities, and inference traffic is shifting from single-shot completions to multi-step, tool-augmented reasoning chains. That shift exposes a real inefficiency in traditional inference routing: recomputing the same shared prefixes over and over across an agent's call chain.

llm-d fixes this at the routing layer, which is where it belongs. By tracking which pods have cached which prefixes, and routing to maximize cache reuse while respecting load constraints, llm-d cuts up to 75% of prefill latency in agentic workloads. The savings compound across multi-turn conversations, multiply across multi-agent platforms, and scale as standardized tool protocols like MCP keep spreading.

The numbers speak for themselves: for an agent making 8 inference calls per execution, llm-d takes total prefill from 3.75 seconds to around 1.1 seconds. At 1,000 concurrent agents, that's nearly 44 GPUs' worth of freed compute per minute. For a cluster of 8x A100 GPUs at $2.21/hr each, cache-aware routing saves about $4,200/month at moderate agent traffic. And as agents get more complex (more tools, longer conversations, deeper multi-step reasoning), the savings only grow.

Beyond raw performance, llm-d makes agent architectures viable that latency used to rule out. ReAct loops can run longer without wrecking the user experience. Tool-augmented generation actually works for interactive apps. Multi-agent orchestration can scale to dozens of concurrent workers without flattening the inference cluster. MCP's whole vision of composable, tool-rich agents only works operationally if the infrastructure can handle the call volume those agents generate — and now it can.

The observability side matters just as much. With OpenTelemetry integration, agentic workloads become visible in a way they've never been. Slow agent execution? The trace shows exactly why — cache miss, tool timeout, overloaded pod. That turns agent performance work from guesswork into actual engineering. You can set SLOs on end-to-end agent latency, track cache hit rates per agent type, and catch routing inefficiencies before users feel them.

Basically, the agentic era needs inference infrastructure that understands the structure of its workloads. Random routing treats every request as independent. Cache-aware routing recognizes that in agentic systems, requests are deeply correlated — shared prefixes, shared sessions, shared tool definitions. llm-d is built to exploit those correlations, and that's why agentic workloads are its killer app.

If you're building agents today without cache-aware routing, you're paying for the same prefill computation 7 times per user request. Your users wait 2 to 4 seconds longer than they need to. Your GPU spend runs 30% to 50% higher than it should. The gap between what agents demand and what traditional LLM serving provides is wide and getting wider. llm-d closes it.

Explore the Interactive Visualizer

I built an interactive visualization of all this — watch requests flow through the EPP in real time, see cache hits and misses happen, and play with different agent configurations.

Open Interactive Visualizer

llm-d is a CNCF Sandbox project for distributed LLM inference routing. It's open source and built for Kubernetes deployments with vLLM backends. More at the llm-d GitHub organization.

Further Reading