Observability for LLM services
The metrics, logs and traces that matter for LLM serving: user-facing and capacity metrics, histograms and percentiles, burn-rate alerts with the arithmetic, structured request records, tracing, dashboards, cardinality and privacy, and quality signals.
You cannot operate what you cannot see. LLM services need the usual three pillars (metrics, logs, traces) plus a few LLM-specific measurements, because the failure modes are different: a service can be fast, healthy and wrong; or slow only for long prompts; or saturated in memory while the CPU is idle.
#The metrics that matter
User-facing (alert on these, tie them to SLOs):
- TTFT histogram (p50, p95, p99)
- Inter-token latency or tokens/s per request
- Request rate, error rate, timeout rate
- Requests finished by reason:
stop,length(hit max tokens),error,cancelled
Capacity (explain why latency moved):
- Requests running and waiting per replica
- KV-cache utilisation (%)
- Prompt tokens/s and generation tokens/s
- Prefix-cache hit rate
- Batch size and preemption or swap counts
- GPU memory, utilisation, temperature, power and error counters
Business:
- Tokens in and out per customer or feature, and cost per million tokens
- Cost per resolved task, where you can define "resolved"
#Where to get them
- vLLM exposes Prometheus metrics at
/metrics(queue length, running requests, cache usage, TTFT and per-token latency histograms). Exact metric names change between versions, so open/metricson your version and note them. - llama.cpp server exposes Prometheus metrics at
/metricswhen started with--metrics. - GPU metrics come from the NVIDIA DCGM exporter (or
nvidia-smifor quick checks). - Gateway metrics give end-to-end latency as users experience it, including queueing before the engine (gateways).
- Application metrics for retrieval (no-result rate, similarity scores) and tool calls (error rate, latency).
#Histograms, percentiles and why averages lie
Suppose 100 requests: 95 take 0.5 s and 5 take 20 s. The mean is 0.95 ร 0.5 + 0.05 ร 20 = 1.5 s. It looks acceptable, yet 5 percent of users waited 20 seconds. The p95 is about 0.5 s (borderline) and the p99 is 20 s: the percentiles show the problem and the mean does not.
Record latency as a histogram with buckets suited to your SLO (for TTFT, for example 0.1, 0.25, 0.5, 1, 2, 5, 10, 30 s). Histograms from many replicas can be summed and then queried for percentiles; averages of per-replica percentiles are wrong. A typical PromQL query (adapt the metric name to your engine):
histogram_quantile(0.95, sum by (le) (rate(ttft_seconds_bucket[5m])))#A starter Prometheus config
scrape_configs:
- job_name: vllm
scrape_interval: 15s
static_configs:
- targets: ["vllm-1:8000", "vllm-2:8000"]
- job_name: gpu
static_configs:
- targets: ["dcgm-exporter:9400"]
- job_name: gateway
static_configs:
- targets: ["gateway:9100"]#Alerts worth having
| Alert | Condition (example) | Meaning |
|---|---|---|
| High TTFT | p95 TTFT above SLO for 5 min | Users are waiting |
| Queue growing | waiting requests above N for 3 min | Need more capacity |
| Cache saturation | KV-cache above 95% for 5 min | Requests will queue or be preempted |
| Error budget burn | error rate above budget burn thresholds | Bad deploy or failing replica |
| GPU fault | ECC or XID errors | Hardware needs replacing |
| Replica down | fewer ready replicas than the minimum | Reduced redundancy |
Saturation in length finishes |
share of finish_reason=length above normal |
Runaway outputs or a prompt change |
| Cost spike | tokens or spend above forecast | Runaway client or prompt bloat |
Alert on symptoms (users hurting) and use capacity metrics for diagnosis. Alerting on every cause creates noise that teams learn to ignore.
#Burn-rate alerts
A better way to alert on an SLO than a fixed threshold is the burn rate: how fast you are consuming the error budget.
budget = 1 โ SLO (99.5% SLO โ 0.5% budget)
burn rate = observed error rate / budgetIf the error rate is 7.2 percent and the budget is 0.5 percent, the burn rate is 14.4. At 14.4 you would consume 14.4 ร (1 hour / 720 hours) = 2% of the 30-day budget in one hour: a good "page now" threshold. A slower leak of 6 times burn consumes 5 percent in 6 hours: a "page soon" level. A burn of 1 consumes exactly the whole budget over 30 days.
A common pattern pairs a long and a short window to avoid noise and to clear quickly:
| Severity | Burn rate | Windows |
|---|---|---|
| Page | 14.4 | 1 hour and 5 minutes both exceeding |
| Page | 6 | 6 hours and 30 minutes |
| Ticket | 1 | 3 days and 6 hours |
You can express "error" as failed requests, requests slower than the TTFT threshold, or both, so a latency SLO uses the same machinery.
#Structured request records
Log a request record per call, as JSON, with a request ID that follows the request through every hop:
{
"ts": "2026-11-11T09:14:22.381Z",
"request_id": "7b1f...",
"route": "/v1/chat/completions",
"model": "support-8b", "release": "support-bot-2026-11-03",
"key_id": "k_42", "tenant": "acme",
"prompt_tokens": 1810, "completion_tokens": 212,
"queue_ms": 35, "ttft_ms": 410, "decode_ms": 2380, "total_ms": 2825,
"finish_reason": "stop", "status": 200,
"cache_hit_tokens": 1500, "retries": 0
}This one table answers most operational questions: who is using what, which release is slow, whether caching is working, which customers drive cost. Log the release ID with every request so a regression lines up with a deploy (rollouts).
Be deliberate about prompt and response text: it can contain personal or confidential data. Either do not log it, sample it, redact it, or protect it with strict retention and access rules (PII, data leakage). Metadata-only logs are the safe default.
#Tracing
Propagate a request ID (and a trace context header) from client to gateway to engine. A trace of queue wait โ prefill โ decode for slow requests tells you immediately whether the fix is capacity (queue), prompt size (prefill) or output length (decode).
| Slow part | Likely cause | Fix |
|---|---|---|
| Queue wait | Saturation | Scale out, shed load, raise limits per replica carefully |
| Prefill | Long prompts, no prefix caching | Shorten prompts, enable caching (prompt caching) |
| Decode | Long outputs, KV pressure, many concurrent users | Cap output, quantise KV, add replicas |
| Gateway or network | Retries, buffering, TLS | Check proxy timeouts and buffering |
| Retrieval or tool step | Slow dependency | Time-box it, cache, add fallbacks |
For RAG and agents, trace each stage (retrieval, rerank, each tool call, each model call) as spans in one trace.
#Cardinality and cost of observability
Every unique combination of label values makes a new time series. Labels like user_id, request_id or full prompt text can create millions of series and bring down your metrics store. Rules:
- Bounded labels only on metrics: model, route, status class, release, tenant tier.
- High-cardinality detail goes in logs and traces, not metrics.
- Set retention by value: high-resolution metrics for days, rollups for months, sampled traces for days.
- Sample traces (for example all errors, all slow requests, 1 percent of the rest).
#Dashboards
Design for the question being asked:
- Overview (for everyone): request rate, error rate, TTFT p95, tokens/s, replicas up. Green or red against the SLO.
- Capacity (for on-call): running and waiting requests, KV-cache use, GPU memory and utilisation, per replica.
- Release (for deploys): metrics split by release ID, canary versus stable.
- Cost (for owners): tokens and spend per tenant or feature, cost per million tokens, utilisation.
- Quality (for the team): sampled evaluation scores, feedback rate, refusal rate, output-length drift.
#Quality is observable too
Latency and errors do not tell you the model got worse. A new quantization, prompt or retrieval index can return 200 OK in 300 ms and be wrong more often. Sample real traffic into your evaluation pipeline and track scores over time, with intervals (online evaluation). Track proxy signals too: refusal rate, average output length, share of length finishes, empty or malformed outputs, feedback ratios.
#Observability checklist
- TTFT, token rate, errors and timeouts as histograms or rates, per release
- Running and waiting requests, KV-cache use, GPU health
- Alerts tied to the SLO with burn rates, plus capacity warnings
- Structured request records with request ID and release ID
- Prompt and response logging policy decided and enforced
- Traces covering queue, prefill, decode and dependencies
- Bounded metric labels; retention set
- Dashboards for overview, capacity, release, cost and quality
- A runbook link in every alert (reliability patterns)
#Common mistakes
- Averages instead of percentiles.
- Alerting on causes (GPU at 90 percent) instead of symptoms (TTFT above SLO).
- Logging full prompts by default.
- High-cardinality labels.
- No release ID on requests, so regressions cannot be tied to deploys.
- Only server-side metrics, missing queueing and network time users experience.
- No quality signal, so a bad model update is invisible.
#Exercise
Your SLO is 99.9 percent of requests with TTFT under 1.5 s over 30 days. In the last hour 1.2 percent of requests exceeded 1.5 s.
- What is the budget and the burn rate?
- How long until the monthly budget is gone at this rate?
- What would you do?
Answers:
- Budget is 0.1 percent. Burn rate
1.2 / 0.1 = 12. - At burn rate 12 the 30-day budget lasts
30 days / 12 = 2.5 days. - Page: this is nearly the "page now" level. Check the capacity dashboard for waiting requests and KV usage, compare by release ID for a recent deploy, and mitigate (scale out, shed load, roll back). Then write the incident up and adjust capacity or limits.
Quick quiz
+10 XP per correct answer, +25 XP for a perfect score
1. Which metric best warns that a server is about to run out of KV-cache memory?
2. Why record latency as histograms rather than averages?
3. Your SLO is 99.5% over 30 days. A burn rate of 14.4 means...
4. Why not label metrics with a user ID?
5. Latency and error rate look perfect, yet users complain the answers got worse. What is missing?