By Adrien Payong and Shaoni Mukherjee

One thing to know: different tools define these terms slightly differently. Some treat TPOT and ITL as the same thing. Others, like NVIDIA’s genai-perf, treat TPOT as an average over the whole generation and ITL as a per-token measurement. Check which definition your tooling uses before comparing numbers across systems.
A production inference service can look completely healthy while some users are already having a bad experience:

But a small, and often commercially important, group of requests can take several seconds just to start responding. That’s the danger of a bad p99 TTFT: the problem is invisible in the averages.
The cause is usually not “the GPU is slow.” It’s more often something happening between request queueing, continuous batching, prompt length, KV-cache allocation, model restarts, routing, and general infrastructure contention. Queueing delay and compute time depend on when a request shows up relative to everything else in the system. Two identical requests, sent to the same model on the same GPU, can have very different TTFTs depending on what else is running at the time.
To debug this well, you need more than a single latency chart. You need to trace the request all the way from the load balancer to the first streamed token, and measure queueing time and compute time separately.
This article walks through a practical, incident-based way to debug p99 TTFT problems in production LLM inference. It covers how to read TTFT alongside TPOT and ITL, five common failure patterns, how to recreate hidden problems by replaying real traffic patterns, and how to pick fixes based on actual data. The goal isn’t just to notice that inference is slow. It’s to find exactly where the delay is coming from and what change removes it.

TTFT ≈ network time + router queue time + scheduler queue time + prefill (compute) time
Say 99 requests finish in about 400ms, but one request with a long prompt takes six seconds to even start. The average latency barely moves, but the p99 blows past your target. If your only dashboard shows the average or the median, you might not even know there’s a problem until users start complaining that the app “freezes” sometimes.
Tail latency is also sensitive to shifts you won’t see in overall throughput. A service could be handling the same number of requests per second, but now with longer prompts, more requests arriving at once, or a different mix of customers. The GPU can keep producing tokens at a normal rate while new requests sit and wait behind ones that are already running.
A good rule of thumb: never look at p99 TTFT by itself. Always check it against queue time, prefill time, prompt length, batch state, KV-cache usage, routing delay, and which instance handled the request.

TTFT, TPOT, and ITL each describe a different part of the request. Comparing them tells you where to look:
Queue time and prefill time are what really tell the two apart. If queue time is the main driver of a slowdown, tuning your model’s kernels won’t help. If queue time looks fine but prefill time grows with prompt length, the problem is in how the model executes.
Continuous batching improves throughput by letting requests join and leave a running batch instead of waiting for a fixed batch to finish. But it doesn’t remove contention. At high concurrency, the scheduler still has to decide between new prefills, running decodes, and what to preempt.
Batching contention usually looks like this:
To confirm this, replay the same mix of prompts at increasing concurrency. Plot p50, p95, and p99 queue time against concurrency. You should see a clear bend in the curve where new requests are arriving faster than the scheduler can admit them within your target latency.

Next, break requests down by prompt length. A single overall percentile can hide the fact that long prompts are slowing down short, interactive ones. Compare prompts under 512 tokens against buckets like 512 to 2048, 2048 to 8192, and anything above 8192 tokens.
Fixing high queue time is usually about admission control, not just making the queue bigger:
If you’re running self-hosted on GPU Droplets, look into chunked prefill, max batch size, max batched-token limits, and keeping long-context traffic in a separate pool from interactive traffic. For Dedicated Inference clusters, use saturation and per-endpoint latency numbers, not just overall GPU utilization, to decide on capacity and routing. DigitalOcean’s Inference platform supports both serverless and dedicated deployments, and the Inference Router (currently in public preview) can send serverless requests to different models based on cost or latency rules you set. Routing can help soak up traffic spikes in some cases, but it shouldn’t be used to hide a saturated endpoint behind an average.
KV-cache memory gets used up by every active sequence. It fills up faster with longer prompts, bigger batches, more concurrency, and longer generations. When free cache blocks get low, the system may start rejecting requests, kicking out active sequences, swapping state to slower memory, or recomputing state it dropped.

This can look like a scheduler problem at first, since new requests spend more time waiting. The way to tell them apart is KV-cache occupancy. If queue time rises as the cache fills up and free blocks disappear, memory pressure, not the scheduler, is the real cause.
Watch for:
Confirm this with two matched tests, one with short prompts and one with long prompts, at the same concurrency. If longer prompts alone cause the cache to fill up and start preempting requests, memory pressure, not the total number of requests, is the bottleneck.
The real fix is planning capacity around token count, not request count. A queue with ten 16K-token prompts is a very different load than one with ten 500-token prompts.
Other options: lower the max context length, cap concurrent sequences, add more cache, switch to a smaller or compressed model, add GPU capacity, or send long-context requests to a separate pool.

Prefix caching can help when prompts share a common prefix, but it’s not a cure-all since unique prompts still use up cache space. A high cache hit rate just means good reuse, it doesn’t mean the cache is under control. Rising occupancy, allocation failures, eviction, and preemption together are what actually signal pressure.
Cold starts shouldn’t be mixed in with normal steady-state latency. A brand-new GPU Droplet, a restarted container, a newly scaled-up replica, or a reloaded model all need time to download weights, allocate GPU memory, set up kernels, build CUDA graphs, or warm up code paths. Requests that land on that replica belong to a different, separate group of latencies.

Signs of this include:
Confirm it by matching slow requests to instance_id, container_start_time, model_loaded_at, model version, and deployment version. If slow requests cluster around recently started replicas for a limited window, it’s a startup issue, not a general lack of capacity.
Make sure your readiness checks actually check that inference is ready to go. A process that’s just listening on a port isn’t necessarily ready to handle a real request. Keep a replica out of the pool until its weights are loaded, memory is allocated, and a test warm-up request succeeds.

During deployments, roll out changes gradually and keep a minimum number of warm replicas available to take traffic. GPU Droplets give you GPU-backed virtual machines for self-managed inference, in single-GPU or 8-GPU setups. On these, you’re responsible for your own model loading and warm-up process.
A request can spend most of its TTFT before it even reaches the inference server. Without timestamps at the edge and application layer, it’s easy to blame the GPU by default, since TTFT feels like an inference metric.

Network-side queueing usually shows up as:
Use a correlation ID that follows the request through the client, load balancer, router, gateway, and inference runtime. Timestamp everything with both wall-clock time and monotonic time. Your traces should clearly show where the request sat waiting.

Streaming setup deserves a closer look too. A proxy might buffer the generated output instead of forwarding the first token right away. In that case, the server will report a healthy first-token time while the client still sees a slow TTFT.

An inference router matters any time requests get sent dynamically to different models or providers. Track which route was picked, how long the routing decision took, how many retries happened, how many fallbacks were tried, and the latency to wherever the request ended up. A fast model can’t make up for a slow router.
GPU workloads can compete for GPU memory and compute, CPU time, host memory, storage bandwidth, or network bandwidth. Even when GPUs are properly isolated, there’s still CPU and network overhead from tokenizing, parsing requests, logging, and streaming output. A noisy-neighbor problem usually looks like:
Always keep per-instance data around. A cluster-wide p99 with no host information just blends one bad replica together with nine healthy ones.

For self-managed setups, compare GPU metrics against host-level ones: utilization, memory use, power state, clock speed, PCIe throughput, CPU load, run-queue depth, memory faults, storage latency, and network errors. Draining a suspected replica and watching if tail latency improves is a quick way to confirm it’s a bad host.

Using dedicated capacity removes some kinds of contention, but it doesn’t remove contention between different workloads sharing the same model endpoint. Keep tenant, route, and workload labels around even on dedicated hardware.

If you don’t have a clean production incident to work from, recreate the problem on a DigitalOcean GPU Droplet instead. This works well because it gives you real, checkable numbers without touching customer data. Set up a fixed model and inference runtime on a specific GPU Droplet configuration, and write down the GPU type, GPU count, model version, numeric precision, tensor-parallel size, inference runtime version, cache settings, and scheduler limits.
Use a load-testing tool like vllm bench serve, genai-perf, or your own async client. Run at least three prompt setups:
Increase concurrency step by step (1, 4, 8, 16, 32, 64, 128) without changing anything else. Let each step run long enough to reach a steady state, and repeat each one at least three times. Collect client-side TTFT, server queue time, prefill time, TPOT, ITL, request throughput, waiting and running sequence counts, KV-cache usage, preemption count, GPU utilization, GPU memory, CPU load, and network latency.
Report the actual numbers you measure, including how much they vary, instead of guessing at expected values. For example:
Under the short-prompt workload, p99 TTFT stayed below [measured value] up to concurrency [value]. With [input length] prompts, KV-cache occupancy hit [value]%, preemptions rose from [value] to [value], and p99 TTFT went from [value] ms to [value] ms. TPOT only changed by [value]%, which points to the regression happening before decoding. Reducing [setting] or adding [capacity change] brought p99 TTFT down to [value] ms under the same load.
Note: the bracketed placeholders above are just an example structure, not real results. Before publishing, replace every placeholder with numbers from an actual benchmark run. Include the full benchmark setup used to get them. This section is the reason this piece is currently blocked. Until CPTO benchmarking capacity is scoped and scheduled for this piece, it should stay in the same holding pattern as the Long-Context Serving Tradeoffs and Inference Observability pieces, not go out with placeholder numbers.
A bad p99 TTFT almost never comes down to one simple “slow inference” number. It’s a symptom that shows up from waiting, prefill cost, cache allocation, model readiness, routing, and general infrastructure contention. The fastest way to find the cause is to break TTFT down at the request level. High queue time points to admission and scheduling. High prefill time points to context length and computation. Cache saturation and preemption point to memory pressure. A mismatch between client and server numbers points to routing or buffering. Slowdowns tied to one replica point to cold starts or noisy neighbors.
DigitalOcean GPU Droplets give you the fine-grained control needed to test and reproduce these problems. Dedicated Inference and the Inference Router offer managed ways to deploy and route traffic, but you should still watch them at the request level. Infrastructure metrics and overall GPU utilization are not a substitute for actually breaking latency down.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
I am a skilled AI consultant and technical writer with over four years of experience. I have a master’s degree in AI and have written innovative articles that provide developers and researchers with actionable insights. As a thought leader, I specialize in simplifying complex AI concepts through practical content, positioning myself as a trusted voice in the tech community.
With a strong background in data science and over six years of experience, I am passionate about creating in-depth content on technologies. Currently focused on AI, machine learning, and GPU computing, working on topics ranging from deep learning frameworks to optimizing GPU-based workloads.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Join the many businesses that use the DigitalOcean AI Platform to accelerate growth. Reach out to our team for assistance with GPU Droplets, 1-click LLM models, AI agents, and bare metal GPUs.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Scale up as you grow — whether you're running one virtual machine or ten thousand.

From GPU-powered inference and Kubernetes to managed databases and storage, get everything you need to build, scale, and deploy intelligent applications.
