Lakshya

LLM Systems Engineering · Chapter 3 of 20

Serving stacks, and prompt caching

Five engines and what each is actually for, plus the caching mechanism where most production money is quietly lost.

6 min read1 diagramAll 20 chapters

Choosing a serving engine looks like a benchmark question and is mostly a workload question. And prompt caching — which nearly every application could use and most use badly — is where the largest easy cost savings live.

The five engines

EngineWhat it actually isPick it when
vLLMThe default open production engine. PagedAttention plus continuous batching, broadest model and hardware coverageThe default. Multi-cloud, non-NVIDIA hardware, models that change often
SGLangA vLLM-class engine whose differentiator is a radix tree over the KV cache (the running notes kept per conversation), giving automatic cross-request prefix reuseTraffic with a shared prefix — chatbots on one system prompt, RAG, agents replaying long histories
TensorRT-LLMA compiler, not just a runtime. Builds a fused hardware-specific engine ahead of timeA stable model on NVIDIA hardware where peak throughput justifies a build step and the lock-in
llama.cppSingle-binary C/C++ inference with GGUF, CPU/Metal/partial offloadLocal, edge, Apple Silicon, CPU-only, no-Python environments
OllamaA packaging and UX layer over llama.cppPrototyping and single-user local development. Not a server

The honest reading of the engine benchmarks

Published comparisons put a radix-tree engine roughly 29% above vLLM on prefix-heavy traffic and within 1 to 4% on unique prompts. Essentially all of the advantage is prefix reuse.

So the advantage is a property of your workload, not of the engine. If your traffic shares prefixes, measure it. If every request is unique, choose on model support and ecosystem instead.

Compiler-based serving is reported 15 to 30% above vLLM at sustained high load, at the cost of a compile step per model and configuration measured in tens of minutes.

And the non-server case is worth stating plainly: a single-user local runtime has no batching at all, so at five concurrent users its tail latency is several times its single-user baseline, while a production engine holds 85 to 92% GPU utilisation at a hundred. People deploy the former to production regularly.

Caveat on all of the above: these projects leapfrog each other release to release, and any specific number has a short shelf life. Teach the mechanism — prefix reuse, compilation, batching — and re-measure the numbers.

Prompt caching: one idea, three vendor skins

Every major provider now caches a prefix of your prompt and charges dramatically less to re-read it — typically about a tenth of the normal input rate. The implementations differ in whether it is automatic, how long it lives, and what invalidates it. The design rule does not differ at all.

GOOD ORDER — THE PREFIX IS STABLE, SO IT CACHES tool definitionssystem promptfew-shot examplesretrieved docsuser turn cached prefix — read at ~0.1× the input rate BAD ORDER — ONE VOLATILE TOKEN AT THE FRONT DESTROYS EVERYTHING AFTER IT “Today is 2026-08-25 14:32”tool definitionssystem promptretrieved docsuser turn cache hit rate: zero. The prefix differs on every request, so nothing after it can match either. THE TRAPS — ALL OF THESE FAIL SILENTLY a prompt below the minimum length — no error, just no cachethe minimum differs per model, and is NOT monotonic by tieran unsorted json.dumps() — key order changes the byteschanging the tool list invalidates system and messages tooN parallel identical requests all miss — the write is not visible yetreasoning effort is an invalidator on some vendors, not just text Every vendor's cache is a prefix cache, so the design rule is identical everywhere regardless of the API: stable content first,volatile content last.
Stable content first, volatile content last. One timestamp at the front of the prefix takes your hit rate to zero, and no amount of cache configuration recovers it.
AnthropicOpenAIGoogle
Opt in?Explicit — cache_control breakpointsAutomatic, on by defaultImplicit on by default; explicit API also available
Read discount~0.1× input~0.1× input~90% off (75% on older models)
Write cost1.25× for 5-minute TTL, 2× for 1-hour—Implicit is free; explicit charges storage per hour
TTL5 minutes or 1 hour~30 minutes on current models~60 minutes default for explicit
Minimum prefixModel-dependent and not monotonic — 512 to 4,096 tokens depending on the model1,024 tokens on current models, 2,048 on older2,048 rising to 4,096 on newer models
BreakpointsUp to 4 explicitn/a — automaticn/a for implicit

The break-even arithmetic is worth doing once. With a write at 1.25× and a read at 0.1×, a five-minute cache pays for itself after a single reuse: 1.25 + 0.1 = 1.35 against 2.0 for two uncached passes. A one-hour cache at 2× write pays off after two reuses. So for anything conversational, caching is essentially always correct.

The traps, all of which fail silently.

Below the minimum length there is no error — you simply get no cache, and the usage field reports zero cached tokens. Check the field rather than assuming.

The minimum is not monotonic across a vendor's model tiers. A newer, larger model can have a lower minimum than an older, smaller one. Porting a caching design between models without re-checking is a real and invisible regression.

Invalidation is tiered by position. Because it is a prefix cache over the rendered order — tools, then system, then messages — changing your tool definitions invalidates everything, while changing the system prompt leaves tools cached. This is why adding one tool to an agent can double your bill overnight.

Non-text settings can invalidate. Reasoning effort, output format and compaction settings are invalidators on some providers. Changing effort mid-session drops your prefix cache even though the prompt bytes are identical.

Concurrent identical requests all miss. The cache entry only becomes readable once the first response starts streaming, so firing N identical requests in parallel pays full price N times. Warm the cache with one request first if you are fanning out.

And the classic: an unsorted json.dumps(), a datetime.now(), a per-user tool list, or a session ID at the front of the prefix. Any of these destroys the cache regardless of how carefully you placed your breakpoints.

The engineering practice that follows: treat the prompt prefix as a build artefact. Construct it deterministically, sort every serialised structure, put anything per-request at the end, and assert on the cached-token count in your integration tests. A cache hit rate is a metric worth a dashboard, because a regression in it is invisible in every other signal and shows up only on the invoice.

Check yourself

What determines whether a radix-tree serving engine beats vLLM?Whether your traffic shares prefixes. On unique prompts the two are within a few percent; the advantage is prefix reuse, which is a workload property.
Where does the prefix cache design rule come from?It is a prefix cache — it matches from the start of the prompt. So anything volatile at the front invalidates everything behind it, regardless of the API.
Why did adding one tool double the bill?Tool definitions sit at position zero of the rendered prompt, so changing them invalidates the system prompt and message history too.
You fired 50 identical requests in parallel and all missed the cache. Why?The cache entry only becomes readable once the first response begins streaming. Warm it with one request before fanning out.
← Quantisation, with the evidenceRetrieval, technically →