LLM Inference Fundamentals for the Routing Engineer
Prefill, decode, KV cache, batching, streaming - the physics behind every latency and cost number
Prefill vs decode
After this you can explain the two phases and why they have different cost/latency profiles.
Every LLM request runs in two phases with completely different physics. Once you see the split, most latency and cost numbers in the deep-dive stop being mysterious.
When a request hits a model, the engine first reads the entire prompt at once, then emits new tokens one by one. The first step is prefill. The second is decode. They stress different parts of the GPU, so they show up as different numbers on your dashboards and different lines on the provider bill.
Prefill takes the whole prompt and runs it through the model in a single forward pass that fills the attention cache and produces exactly one token: the first one. Because every prompt token is processed in parallel, this phase is compute-bound - it saturates the GPU's matrix-multiply units. It is what you wait on before anything appears on screen, so it sets the time-to-first-token (TTFT).
Decode then generates the rest, token by token, each new token attending to everything before it. There is no parallelism across the sequence here: token N must exist before token N+1 can be computed. The bottleneck is moving model weights and the cache through GPU memory, so this phase is memory-bandwidth-bound. It sets inter-token latency (ITL) and, multiplied across hundreds of output tokens, most of the total wall-clock time on a long answer.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Step through a single request: prompt in, one prefill pass, then a decode loop that emits the rest.
- Property
- What it does
- Prefill
- Process the whole prompt, emit token 1
- Decode
- Emit each subsequent token autoregressively
- Property
- Parallelism
- Prefill
- All prompt tokens at once
- Decode
- Strictly sequential, one token per step
- Property
- Bottleneck
- Prefill
- Compute (matmul / FLOPs)
- Decode
- Memory bandwidth (weights + KV through HBM)
- Property
- Latency metric it drives
- Prefill
- TTFT (time-to-first-token)
- Decode
- ITL (inter-token latency)
- Property
- Scales with
- Prefill
- Prompt / context length
- Decode
- Number of output tokens
- Property
- Cost line it maps to
- Prefill
- Input-token pricing
- Decode
- Output-token pricing
| Property | Prefill | Decode |
|---|---|---|
| What it does | Process the whole prompt, emit token 1 | Emit each subsequent token autoregressively |
| Parallelism | All prompt tokens at once | Strictly sequential, one token per step |
| Bottleneck | Compute (matmul / FLOPs) | Memory bandwidth (weights + KV through HBM) |
| Latency metric it drives | TTFT (time-to-first-token) | ITL (inter-token latency) |
| Scales with | Prompt / context length | Number of output tokens |
| Cost line it maps to | Input-token pricing | Output-token pricing |
Two phases, two bottlenecks - internalize this table and the rest of the module follows.
Why a routing engineer caresthe editor surfaces feel these two numbers differently
Cursor's surfaces have wildly different latency profiles and that difference is really a prefill-vs-decode difference. Tab completion lives or dies on TTFT - the suggestion has to appear before the developer's fingers move on, inside a sub-100ms budget. A long Agent generation cares far more about sustained ITL and throughput, because the user is reading a stream and the total time is dominated by hundreds of decode steps.
Short prompt, short output
TTFT is everything (sub-100ms feel)
Prefill-sensitive, decode is tiny
Medium prompt, focused edit
TTFT still dominates perceived speed
A few hundred ms is acceptable
Large context, long output
ITL + throughput dominate total time
Prefill on big repo context can hurt TTFT too
Long prompts are where the two phases collide. Stuff a large repo or a 100k-token context into a request and prefill cost explodes, because it scales with prompt length. That is a routing and caching problem before it is a model problem: the same repo context resent on every keystroke is the single biggest lever you have. We will pull that lever in the next section.
In the deep-dive you should be able to draw, freehand, where time and money go: a short prompt with a long answer is decode-dominated (output tokens, ITL); a huge context with a one-line answer is prefill-dominated (input tokens, TTFT). Naming which phase dominates a given workload is the move that signals you actually understand inference rather than reciting terms.
Do not say a request is "slow because the model is big" and stop there. The interviewer wants the phase: is TTFT bad (prefill - long context, cold cache, queueing) or is ITL bad (decode - memory bandwidth, large batch contention)? The fix is different in each case and conflating them reads as surface knowledge.
Takeaway. Prefill is compute-bound and sets TTFT; decode is memory-bandwidth-bound and sets ITL - Tab cares about the first, Agent about the second.
Self-check
QWhich statement about the two inference phases is correct?
KV cache and prefix caching
After this you can reason about KV cache as the central cost/latency lever.
The KV cache is the single most important object in inference economics. It is why decode is fast, it is your tightest memory constraint and prefix caching is the biggest cost lever Cursor has.
Attention needs the keys and values of every previous token to compute the next one. Without a cache, each new token would re-run attention over the entire sequence from scratch - quadratic work that would make decode hopelessly slow. The KV cache stores those keys and values per token, so generating token N is cheap: attend to the cached state plus the one new token.
That speed has a price in memory. The cache grows linearly with context length and with how many requests share the GPU at once.
- Roughly proportional to
- context_length × batch_size × layers × hidden_size × 2 (K and V)
- Grows with
- Longer contexts and more concurrent requests
- Lives in
- GPU HBM, alongside model weights - they compete for the same space
- Why it bounds concurrency
- When KV memory fills, you can't admit another request - this caps batch size
The KV cache, not raw FLOPs, is usually what limits how many requests one GPU can serve.
Because the cache is the capacity ceiling, how you manage its memory directly sets how many concurrent users a GPU can hold. Naive allocation reserves the full context window per request up front, which wastes huge amounts of memory on requests that finish short. PagedAttention, introduced by vLLM, fixes this by chopping the cache into fixed-size pages allocated on demand, the way an OS pages physical memory. Fragmentation drops, utilization rises and effective batch size goes up without buying more GPUs.
Prefix caching: reuse the work you already didthe lever with Cursor's name on it
If two requests start with the same tokens, their KV cache for that shared span is identical. Prefix caching keeps those computed keys/values around and reuses them, so a request that shares a prefix skips prefilling it entirely. The first matching token is then the only new prefill work.
Cursor's prompts are unusually repetitive, which is exactly the condition prefix caching loves. The same system prompt, the same tool definitions and often the same file or repo context get sent across many requests in a session. A high prefix-cache hit rate turns an expensive full prefill into a near-free cache read.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
The same request, two cache outcomes - and why affinity routing decides which one you get.
Full prefill of system prompt + context
Pay input-token compute for every token
TTFT includes the whole prefill pass
Reuse cached KV for the shared prefix
Prefill only the new suffix
TTFT and input cost drop sharply
Connect prefix caching to routing explicitly: prefix-cache affinity means "send requests that share a prefix to the same instance." If you load-balance round-robin, every instance misses on the shared prefix and you pay full prefill everywhere. Affinity-aware routing - hashing on the prefix or session - is a real lever a routing engineer owns and naming it shows you connect mechanics to the system you'd build.
Prefix caching only helps for an exact token-prefix match from position zero. Change one token early in the prompt - reorder the system message, inject a timestamp, vary the tool list - and the cache misses from that point on. "Keep the stable, shared content at the front and the volatile content at the back" is the correctness rule that makes the cache actually pay off.
Takeaway. KV-cache memory caps concurrency; prefix caching plus affinity-aware routing turns Cursor's repetitive prompts into cheap cache hits.
Self-check
QWhy does prefix-cache affinity push you away from naive round-robin load balancing?
Batching and the throughput/latency tradeoff
After this you can explain continuous batching and its effect on tail latency.
Batching is how a GPU serves many users at once instead of one. How you batch is the throughput-versus-latency knob at the heart of serving - name it explicitly and the design round opens up.
GPUs are fastest when they process many sequences together, amortizing the cost of loading weights across the whole batch. The question is when you form the batch and what you do when sequences in it finish at different times.
- Approach
- Static batching
- How it works
- Wait until a full batch of requests arrives, run them together to completion
- Cost
- Latecomers wait for the batch to fill; the whole batch waits for the slowest sequence
- Approach
- Continuous / in-flight batching
- How it works
- Add and remove sequences every decode step; a finished sequence is swapped out and a queued one swapped in immediately
- Cost
- Far higher GPU utilization, much less head-of-line blocking
| Approach | How it works | Cost |
|---|---|---|
| Static batching | Wait until a full batch of requests arrives, run them together to completion | Latecomers wait for the batch to fill; the whole batch waits for the slowest sequence |
| Continuous / in-flight batching | Add and remove sequences every decode step; a finished sequence is swapped out and a queued one swapped in immediately | Far higher GPU utilization, much less head-of-line blocking |
Continuous batching (vLLM, TGI, TensorRT-LLM) is the modern default for exactly this reason.
The win from continuous batching is that the GPU never idles waiting for the slowest sequence and a short request stuck behind a long one doesn't wait for the long one to finish. Each decode step re-forms the working set, so capacity tracks demand step by step.
The tradeoff you must name out loudbigger batches are not free
Pushing batch size up raises throughput and GPU utilization - more tokens per dollar. It also raises per-request and tail latency, because each request now shares memory bandwidth and compute with more neighbors and the decode step for a larger batch takes longer. This is the throughput/latency tradeoff and serving research (vLLM, Sarathi-Serve) is largely about pushing what's new of it.
More throughput, higher GPU utilization
Lower cost per token
Worse p99 / ITL under load
Tighter, more predictable tail latency
Better for latency-critical Tab traffic
Lower utilization, higher cost per token
Sarathi-Serve's contribution is worth a sentence: it chunks long prefills and interleaves them with ongoing decodes so a big prefill doesn't stall everyone's token stream. The general principle is that prefill and decode compete for the same GPU and how you schedule them against each other shapes tail latency.
What this means for provider-backed routingyou don't own their batcher, but you own the dials in front of it
When you route to a third-party provider, their serving stack does the batching and you can't see inside it. What you control is upstream: concurrency limits, queueing and admission, hedging and which provider gets a given request. Those decisions interact with the provider's batch composition even though you never touch it.
- Concurrency limit
- How many in-flight requests you allow per provider/route
- Admission / queueing
- Whether a request enters now, waits or is shed under load
- Hedging
- Sending a duplicate to a second provider to cut tail latency, at extra cost
- Provider selection
- Which backend a request lands on, by health, latency, price, cache affinity
When you reach a batching decision in a design round, say the words "this is the throughput-versus-latency tradeoff" and then anchor it to the workload: "For Tab I'd cap batch size and concurrency to protect p99; for bulk Agent generations I'd allow larger batches because throughput per dollar matters more than a few ms of ITL." Tying the dial to the specific surface is the senior signal.
Don't claim continuous batching "removes" the latency cost of large batches. It removes the waiting and the head-of-line blocking of static batching; it does not repeal the fact that a bigger concurrent batch makes each decode step slower. Mixing those up is a common tell.
Takeaway. Continuous batching keeps the GPU busy and kills head-of-line blocking, but larger batches still trade p99 latency for throughput - at a provider you tune that via concurrency, admission and hedging.
Self-check
QYou route to a third-party provider whose internal batching you can't control. Which set of levers is genuinely yours at the gateway?
Streaming, tokens and context limits
After this you can handle streaming responses and tokenization correctly in a gateway.
A gateway that buffers a full response before sending it has already lost. Streaming, token accounting and cancellation are where inference mechanics become concrete gateway code.
Models emit output token by token, almost always over Server-Sent Events (SSE). The whole point is that the user sees text appear as it's generated. A gateway must pass those chunks through as they arrive, not collect the full answer and forward it at the end - buffering would throw away the streaming UX and inflate perceived latency to total latency.
- 1Open upstream as a stream. Request SSE from the provider and read the response body incrementally, never
awaitthe whole thing into memory. - 2Flush each chunk downstream. Forward provider events to the client as they land, preserving event boundaries; do not coalesce or reorder.
- 3Measure both clocks. Record TTFT at the first token and total latency at stream end - they're different numbers and users feel the first one.
- 4Propagate completion and errors. Send the terminal event on success and on an upstream error mid-stream, surface it cleanly rather than hanging the connection.
- 5Wire cancellation through. If the client disconnects or aborts, abort the upstream request so you stop generating (and paying for) tokens nobody will read.
// AbortController lets a client cancel propagate to the provider.
async function proxyStream(req: Request, provider: Provider): Promise<Response> {
const ac = new AbortController();
req.signal.addEventListener("abort", () => ac.abort()); // client gone -> stop upstream
const upstream = await provider.fetch({ signal: ac.signal, stream: true });
if (!upstream.body) throw new GatewayError("no stream body");
let firstToken = false;
const t0 = performance.now();
const out = upstream.body.pipeThrough(
new TransformStream({
transform(chunk, controller) {
if (!firstToken) { firstToken = true; recordTTFT(performance.now() - t0); }
controller.enqueue(chunk); // flush immediately, no buffering
},
}),
);
return new Response(out, { headers: { "content-type": "text/event-stream" } });
}Tokens are not characters and they're not portablecounting is per-provider
Tokenization differs by model and provider - the same string is a different token count under different tokenizers. Token counts drive three things you can't fudge: cost estimates (pricing is per token), context-limit checks (does this request fit?) and rate-limit accounting (TPM budgets). A gateway that estimates tokens with the wrong tokenizer will misprice requests and either reject valid ones or let oversized ones through.
- Cost estimation
- Per-token input/output pricing - wrong count, wrong bill prediction
- Context-limit checks
- Reject or down-route before the provider 400s on an oversized prompt
- Rate-limit accounting
- TPM (tokens-per-minute) budgets need real counts, not character heuristics
Context windows are finite, so the gateway needs a graceful answer when a request won't fit. Two honest options: reject early with a clear error or down-route to a model with a bigger window (and a different price). Silently truncating the prompt is the wrong move - it changes the user's request without telling them.
When a user hits stop on an Agent run, the gateway must propagate that cancellation upstream. If it doesn't, the provider keeps decoding tokens the user will never see and you keep paying output-token prices for them. "Client abort fans out to an AbortController on the upstream request" is the pattern - and it's both a UX and a real-dollars concern at Cursor's volume.
Counting tokens by dividing characters by four is fine for a rough log line and dangerous for billing or admission. For anything load-bearing, use the model's actual tokenizer or the provider's usage numbers, because the heuristic drifts hard on code, non-English text and long identifiers.
Takeaway. Stream chunks through without buffering, count tokens with the right tokenizer for cost/context/rate limits and fan client cancellation out to the provider so you stop paying for unread tokens.
Self-check
QWhy can't a gateway rely on a single global "characters ÷ 4" rule to count tokens for billing and admission?
GPU economics and provider model
After this you can connect inference mechanics to dollars and capacity.
Everything in the previous four sections eventually becomes a number on a bill or a capacity plan. The routing engineer who can do this math is the one who earns the gray-area decisions.
GPU cost is mostly about keeping expensive silicon busy. Utilization is driven by batch size and how well the KV cache fits in memory: idle GPUs and tiny batches burn rent for nothing, while a well-packed batch spreads the fixed cost of loading weights across many requests. The mechanics from earlier sections - paged KV, continuous batching, prefix-cache hits - are the same levers that move utilization.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Each layer rests on the one below: cost is set at the bottom, then earned back by how well the layers above pack it.
- Per-token pricing
- Separate input (prefill) and output (decode) rates; output is usually pricier
- Rate limits
- RPM (requests/min) and TPM (tokens/min) caps per key/account
- Reserved vs on-demand
- Committed/provisioned capacity (cheaper, fixed) vs on-demand (flexible, pricier, throttled)
- GPU utilization
- Driven by batch size and KV-cache fit - the core efficiency metric for self-hosting
- Cache hit rate
- Fraction of prefix work served from cache - a first-class cost metric, not a footnote
Self-hosted vs provider-hostedthe tradeoff that frames the whole role
This is the strategic fork the inference platform lives on. Hosting your own models on rented or owned GPUs gives you control and the best cost-at-scale, at the price of running a serving stack, capacity planning and on-call. Calling a provider's API gives you flexibility and operational simplicity, at the price of per-token margin, rate limits and being exposed to their outages. A mature platform does both and routes between them.
- Dimension
- Cost at scale
- Self-hosted
- Lowest if utilization is high
- Provider-hosted
- Per-token margin baked in
- Dimension
- Control / tuning
- Self-hosted
- Full (batching, KV paging, quantization)
- Provider-hosted
- Limited to API knobs
- Dimension
- Operational burden
- Self-hosted
- You run serving + on-call + capacity
- Provider-hosted
- Provider runs it
- Dimension
- Flexibility / new models
- Self-hosted
- You must deploy each one
- Provider-hosted
- New models appear behind one API
- Dimension
- Failure exposure
- Self-hosted
- Your fleet, your blast radiusHow much breaks if a change goes wrong; the scope of potential damage.
- Provider-hosted
- Their outage degrades you unless you fail over
| Dimension | Self-hosted | Provider-hosted |
|---|---|---|
| Cost at scale | Lowest if utilization is high | Per-token margin baked in |
| Control / tuning | Full (batching, KV paging, quantization) | Limited to API knobs |
| Operational burden | You run serving + on-call + capacity | Provider runs it |
| Flexibility / new models | You must deploy each one | New models appear behind one API |
| Failure exposure | Your fleet, your blast radiusHow much breaks if a change goes wrong; the scope of potential damage. | Their outage degrades you unless you fail over |
The honest answer in an interview is "it depends on volume, latency budget and which models" - then reason it.
Capacity planning, back-of-the-envelopefind the bottleneck before you build
The deep-dive often ends with a sizing question. The method is to estimate demand in the same units as your limits, then see what hits the ceiling first.
- 1Estimate demand. Peak QPS × average tokens per request (input + output) gives you tokens/sec and requests/sec at peak.
- 2Compare to provider limits. Check that demand against RPM and TPM caps - TPM usually binds first for long-context traffic.
- 3Compare to GPU memory. For self-hosting, peak concurrency × KV per request must fit in HBM alongside weights or batch size collapses.
- 4Name the binding constraint. Say which ceiling you hit first (TPM, RPM or KV memory) - that's the one your design has to relieve.
Because prefill is paid per input token, a few points of prefix-cache hit rate move the bill materially at Cursor's volume - the same system prompts and file contexts recur constantly. Treating cache hit rate as a first-class SLI you route to improve, not an implementation detail, is exactly the cost-at-scale fluency the JD asks for.
"I'd size against TPM first since our long-context Agent traffic is token-heavy, then check KV-cache memory for the self-hosted tier. My first cost lever is prefix-cache hit rate via affinity routing, because prefill on repeated repo context is where the input-token spend concentrates. Self-hosted for the steady high-volume models where utilization stays high, provider APIs for spiky or new models - and failover between them so no single backend outage is user-visible."
Don't optimize utilization into a latency violation. Cranking batch size to push GPU utilization toward 100% is a great way to blow the Tab p99 budget. The right framing is "highest utilization that still meets the latency SLO," with the answer differing by surface - high-utilization batches for bulk Agent work, leaner batches for latency-critical Tab.
Takeaway. Cost = keeping GPUs busy without breaking the latency SLO; size against TPM and KV memory, treat prefix-cache hit rate as a money metric and split self-hosted vs provider by volume and latency budget.
Self-check
QWhy is prefix-cache hit rate treated as a first-class cost metric rather than an implementation detail?