Original source
---
id: inference-load-testing
title: Inference Metrics and Load Testing
code: IM
subtitle: Find why a load test lies. TTFT, TPOT, goodput, P99, realistic prompts, retry storms and hedged reads.
hue: 352
sources: phases/17-infrastructure-and-production/08-inference-metrics-goodput, phases/17-infrastructure-and-production/22-load-testing-llm-apis, projects/retry-storm-lab, projects/hedged-request-lab
source_hash: 26e858df4a14ef8f
---
## Symptoms, causes, fixes
type: troubleshooting
from: phases/17-infrastructure-and-production/08-inference-metrics-goodput, phases/17-infrastructure-and-production/22-load-testing-llm-apis, projects/retry-storm-lab, projects/hedged-request-lab
| 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
type: annotated-code
lang: text
span: 2
from: phases/17-infrastructure-and-production/08-inference-metrics-goodput/docs/en.md
```text
TTFT = queue_time + network_request + prefill_time # before anything streams
TPOT = (decode_forward_time + scheduler_overhead) / tokens_produced # same as ITL
E2E = TTFT + TPOT * output_tokens + network_response # TPOT-dominated above 500 tokens
throughput = total_output_tokens / elapsed_time # fleet efficiency only
goodput = fraction of requests meeting (TTFT <= a) AND (TPOT <= b) AND (E2E <= c)
```
## SLO profiles in main.py
type: comparison
from: phases/17-infrastructure-and-production/08-inference-metrics-goodput
| 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
type: comparison
span: 2
from: phases/17-infrastructure-and-production/22-load-testing-llm-apis/docs/en.md
| 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
type: concepts
from: phases/17-infrastructure-and-production/22-load-testing-llm-apis/docs/en.md
- **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.
## Run the labs
type: annotated-code
lang: bash
span: 2
from: projects/retry-storm-lab/README.md, projects/hedged-request-lab/README.md
```bash
python3 scripts/project_test.py retry-storm-lab --init /tmp/retry-storm-lab-work
python3 scripts/project_test.py retry-storm-lab --all --solution --strict
cd projects/retry-storm-lab/solution
go run . --clients 20 --budget 60 --jitter=false --out /tmp/synchronized.json # synchronized waits
go run . --clients 20 --budget 60 --jitter=true --seed 7 --out /tmp/jittered.json # seeded spread
go run . --local-http --clients 20 --budget 12 --out /tmp/retry-wire.json # real loopback HTTP
node --experimental-strip-types cli.ts /tmp/retry-wire.json # verify counters
cd projects/hedged-request-lab/solution
go run . --samples 3 --delay-ms 10 --out /tmp/hedge-report.json # 80ms primary, 5ms secondary
```
## Lab flag defaults
type: defaults
from: projects/retry-storm-lab/solution/main.go, projects/retry-storm-lab/README.md, projects/hedged-request-lab/solution/main.go, projects/hedged-request-lab/README.md
| 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 |
## Lab error strings
type: command-table
from: projects/retry-storm-lab/solution/main.go, projects/hedged-request-lab/solution/main.go
| 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 |
## One hedged read
type: flow
from: projects/hedged-request-lab/stages, projects/hedged-request-lab/README.md
1. **Client**: starts the primary GET and a delay timer.
2. **Client**: a valid primary before the timer prevents the hedge.
3. **Client**: else launches at most one secondary. Delay is 0 to 1 second.
4. **Validator**: only HTTP 200 with a nonempty JSON value can win.
5. **Client**: cancels the loser and observes it for at most 100ms.
6. **Receipt**: records late and unobserved attempts and winner latency.
7. **Fixture**: counts server work that continues after cancel.
## Worked numbers
type: qa
from: phases/17-infrastructure-and-production/08-inference-metrics-goodput, projects/retry-storm-lab/stages, projects/hedged-request-lab/stages
- **TTFT 500 ms, 100 tokens in 700 ms decode?** GenAI-Perf `ITL = 700/99 = 7.07 ms`. LLMPerf `ITL = 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
type: tips
from: phases/17-infrastructure-and-production/08-inference-metrics-goodput/docs/en.md, phases/17-infrastructure-and-production/22-load-testing-llm-apis/docs/en.md
- 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.