Inference Metrics and Load Testing
Find why a load test lies. TTFT, TPOT, goodput, P99, realistic prompts, retry storms and hedged reads.
- Concepts and terms
- Tasks and commands
- Annotated code
- Diagrams
- Comparisons
- Defaults and numbers
- Troubleshooting
- Questions and tips
Symptoms, causes, fixes#
| Symptom | Cause | Fix |
|---|---|---|
| Test held at 500 users, prod fell at 200 | Identical prompts hit the prefix cache | --mean-input-tokens plus --stddev-input-tokens |
| Locust ITL high, server is fine | GIL trap: client tokenization backlog | LLM-Locust or a compiled harness |
| GenAI-Perf TPOT 6 ms, LLMPerf 11 ms | GenAI-Perf excludes TTFT from ITL | State the tool and the definition |
| High throughput, users leave | Low goodput, tail blows the SLO | Gate deploys on goodput |
| Mean TPOT 9 ms, users complain | Right-skewed tail, P99 TPOT 65 ms | Report P50, P90, P99 |
| TPOT spikes to 50 ms | Long prefill on a neighbor sequence | Chunked prefill |
| All clients retry at the same time | Synchronized exponential waits | --jitter=true and a shared --budget |
| Hedge looks free | Cancel does not stop server work | Count fixture work next to latency |
Metric formulas#
TTFT = queue_time + network_request + prefill_timebefore anything streamsTPOT = (decode_forward_time + scheduler_overhead) / tokens_producedsame as ITLE2E = TTFT + TPOT * output_tokens + network_responseTPOT-dominated above 500 tokensthroughput = total_output_tokens / elapsed_timefleet efficiency onlygoodput = fraction of requests meeting (TTFT <= a) AND (TPOT <= b) AND (E2E <= c)SLO profiles in main.py#
| Factor | loose | target | tight |
|---|---|---|---|
| TTFT ms | 800 | 500 | 300 |
| TPOT ms | 25 | 15 | 10 |
| E2E ms | 3000 | 2000 | 1500 |
| Verdict | SHIPPABLE at 99% | DEGRADED at 95% | FAILING below |
Four load patterns#
| Factor | Steady-state | Ramp | Spike | Soak |
|---|---|---|---|---|
| Shape | Constant RPS | 0 to target RPS | Sudden 3-10x RPS, then back | Steady-state |
| Duration | 30-60 min | 15 min | 2 min | 4-8 hours |
| Catches | Baseline regressions | Capacity breakpoint, warm-up anomalies | Autoscaling latency, queue saturation, cold start | Memory leaks, connection-pool drift |
Use each tool when#
- LLMPerf
- a benchmark run. Rust-backed tokenization, mean/stddev prompts.
- GenAI-Perf
- you need the NVIDIA reference. ITL excludes TTFT.
- LLM-Locust
- your team uses Locust. It fixes the GIL trap.
- guidellm
- large-scale synthetic benchmarking.
- k6 v2026.1.0
- CI/CD gates and SLA tests. Streaming-aware, no GIL.
- k6 Operator
- distributed tests through TestRun / PrivateLoadZone CRDs.
- Vegeta
- constant-rate saturation of a gateway or rate limit.
- Locust 2.43.3 stock
- never alone for LLMs. It has the GIL trap.
Lab error strings#
| Command | What it does |
|---|---|
invalid bounded policy | Base below 1, cap below base, or attempts over 10 |
non-retryable HTTP status | Status is not 429, 502, 503 or 504 |
server delay exceeds policy cap | Retry-After is above the policy cap |
retry attempts or shared budget exhausted | No attempts or budget left |
winner requires HTTP 200 | A fast error cannot win the hedge |
body exceeds 64 KiB | Response body is over the read limit |
no valid response | Both hedge attempts failed |
samples must be 1..100 | Sample count is out of range |
Run the labs#
python3 scripts/project_test.py retry-storm-lab --init /tmp/retry-storm-lab-workpython3 scripts/project_test.py retry-storm-lab --all --solution --strictcd projects/retry-storm-lab/solutiongo run . --clients 20 --budget 60 --jitter=false --out /tmp/synchronized.jsonsynchronized waitsgo run . --clients 20 --budget 60 --jitter=true --seed 7 --out /tmp/jittered.jsonseeded spreadgo run . --local-http --clients 20 --budget 12 --out /tmp/retry-wire.jsonreal loopback HTTPnode --experimental-strip-types cli.ts /tmp/retry-wire.jsonverify counterscd projects/hedged-request-lab/solutiongo run . --samples 3 --delay-ms 10 --out /tmp/hedge-report.json80ms primary, 5ms secondaryLab flag defaults#
| Setting | Default | Note |
|---|---|---|
--clients | 20 | Concurrent clients |
--budget | 12 | Shared retry attempts |
--base-ms | 10 | Initial backoff |
--max-ms | 80 | Maximum retry wait |
--attempts | 4 | Maximum attempts per client |
--capacity | 5 | Simulated capacity per exact millisecond |
--delay-ms | 10 | Hedge delay |
--samples | 3 | Samples per policy |
One hedged read#
Worked numbers#
- TTFT 500 ms, 100 tokens in 700 ms decode?
- GenAI-Perf
ITL = 700/99 = 7.07 ms. LLMPerfITL = 1200/100 = 12.00 ms. - Twenty clients, four attempts, budget 12?
- At most 32 total calls.
- Base 10, cap 25, no jitter?
- Waits of 10,20,25,25.
- Three baseline and three hedged samples?
- Three calls versus six. The fixture records nine.
- Samples [2,4,10,20,80]?
- P50=10, P95=80, maximum=80.
- Primary HTTP 500 at 1ms, secondary valid at 15ms?
- The secondary wins. A fast error cannot win.
Reference numbers and gates#
- Llama-3.1-8B-Instruct on TRT-LLM: mean TTFT 162 ms, mean TPOT 7.33 ms.
- Consumer 70B SLO: TTFT P99 <= 800 ms, TPOT P99 <= 25 ms, goodput >= 99%.
- Run at 2x peak concurrency. Take 30-50 iterations.
- Use
--mean-input-tokens 800 --stddev-input-tokens 300 --mean-output-tokens 150. - CI gate: P50/P95 TTFT, 5xx < 5%, TPOT under threshold. Break the build on breach.
- Publish tool name, tool version, model, hardware, concurrency and prompt distribution.