Kiln measures token timing per request. Tokens from concurrent streams are never paired with one another, so an inter-token latency (ITL) sample always describes one request.
Use this page to answer three different questions:
| Question | Start here |
|---|---|
| How long did one request wait for its first token? | ttft_ms in response metadata or the recent-request view |
| Where did a pause between two tokens occur? | kiln.token_timing, request phase totals, and stall reasons |
| Is tail latency changing across the server? | GET /v1/stats/decode and GET /metrics |
These surfaces provide diagnostic wall-clock evidence. They are not device-kernel profiles, and they do not all measure the same population.
Choose an observation surface
| Surface | Scope | Important limitation |
|---|---|---|
Response metadata.performance.latency |
One completed request | Available only when performance metadata is enabled |
kiln.token_timing SSE events |
One token at a time in one stream | Kiln-specific events; require explicit request opt-in |
GET /v1/stats/recent-requests |
Bounded recent request history | Can include prompt and completion text; protect this endpoint |
| Dashboard Latency diagnosis | Human-readable recent-request drill-down | Shows the same bounded server-side observations |
GET /v1/stats/decode |
Request-local gaps retained from the last 60 seconds | Decode cadence, not end-to-end throughput |
GET /metrics |
Process aggregates | Fixed-cardinality histograms and counters, not request traces |
Kiln does not authenticate the observability endpoints. Keep the server on loopback or place it behind an authenticated reverse proxy, especially when recent-request data is enabled.
Enable request timing
Set include_performance on a chat request:
{
"model": "Qwen3.5-4B",
"messages": [
{
"role": "user",
"content": "Explain the pause."
}
],
"stream": true,
"include_performance": true
}
For a non-streaming real-model request, Kiln adds the summary at
metadata.performance.latency. For a streaming real-model request, the
terminal chat chunk carries the same summary.
An explicit request value of include_performance: true also adds a
kiln.token_timing event after each emitted model token. The server-wide
performance-metadata default can enable terminal metadata, but it does not
silently add Kiln-specific objects to an otherwise OpenAI-compatible stream.
Leave the request field false or omitted when a client accepts only standard
chat chunks.
Understand the clocks
All request boundaries use one process-local monotonic clock. Fields ending in
_ms are floating-point milliseconds.
| Boundary | Exact meaning |
|---|---|
| Request receipt | Chat request handling begins, before prompt rendering and tokenization |
| Ready | The batching actor has produced an accepted token |
| Producer delivered | The delivery worker enqueues the token for the HTTP handler |
| Handler received | The asynchronous chat producer receives the token event |
| Body enqueued | The producer enqueues rendered content into the HTTP response body |
body_enqueued does not mean that the remote client received or rendered the
bytes. TCP buffers, proxies, network transit, and client work are outside
Kiln's clock.
The compatibility field queue_delay_ms is:
response_delivery_ms + handler_queue_ms
The public name client_delivery_ms is also retained for compatibility. Its
actual boundary is handler receipt to response-body enqueue—not delivery to
the remote client.
Read a request summary
The summary reports:
| Field | Meaning |
|---|---|
emitted_tokens |
Generated tokens emitted by this request, including the first token |
gap_samples |
Lifetime count of gaps between consecutive tokens from this request |
retained_gap_samples |
Gaps still retained for percentile and stall calculation |
gap_samples_truncated |
Whether older samples were evicted from the bounded request buffer |
ttft_ms |
Request receipt to the first ready token |
itl_ms_p50, itl_ms_p99, itl_ms_p999 |
R-7 percentiles over retained request-local gaps |
max_itl_ms |
Largest retained request-local gap |
stall_threshold_ms |
Adaptive request threshold: max(250 ms, 5 × p50 ITL) |
stall_count |
Retained gaps at or above that threshold |
unexplained_stall_count |
Stalls for which no measured phase explains enough of the gap |
stall_reasons |
Stall counts by dominant measured reason |
phases |
Request-impact time by bounded phase |
gap_samples is normally emitted_tokens - 1. Kiln retains at most 8,192
gaps per request. After that limit, percentiles and stall counts describe the
retained suffix while gap_samples continues to count the full request.
A request that emits no token has null TTFT and ITL values. A one-token request has TTFT but no ITL sample.
Interpret phase attribution
| Phase | Current boundary |
|---|---|
actor_queue_ms |
API enqueue until the batching actor begins admission |
actor_admission_ms |
Actor work that prepares and admits the request |
tokenization_ms |
Prompt encoding in the request handler |
prefill_ms |
Actor prefill wall time while the request is active |
decode_ms |
Actor decode wall time while the request is active |
actor_cycle_idle_ms |
Configured cooperative actor idle time while the request remains active |
sampling_ms |
Separately measurable final normalization, LM-head, filtering, and token selection |
readback_ms |
An existing device-to-host token read when the backend exposes that boundary |
response_delivery_ms |
Ready token to delivery-worker handoff |
handler_queue_ms |
Delivery-worker handoff to handler receipt |
client_delivery_ms |
Handler receipt to response-body enqueue |
gpu_lock_wait_ms |
Time waiting for shared inference GPU ownership |
graph_capture_ms |
Backend graph-capture work attributable to this request |
graph_replay_ms |
Backend graph-replay work attributable to this request |
synchronization_ms |
Backend work settlement at an external-yield boundary |
resize_ms |
KV-resize work that delayed request admission or overlapped GPU-lock waiting |
trim_ms |
Allocator trim that overlapped GPU-lock waiting |
adapter_ms |
Adapter mutation that delayed request admission |
training_ms |
Training GPU ownership that overlapped inference-lock waiting |
unexplained_ms |
Inter-token wall time not covered by a measured candidate |
Preserve the distinction between these values:
nullmeans the path did not observe or could not isolate that phase;0means the path measured the phase at zero at the reporting resolution;- a positive value is request-impact wall time, not necessarily exclusive device work.
Phase values can overlap. decode_ms can contain narrower backend phases, and
delivery can overlap later actor work. Never sum every phase and compare the
result with request duration.
A shared batched invocation can be attributed to every ready request that participated in it. Request phase totals therefore explain causal overlap; they are not process-wide work totals. Use backend lifetime telemetry when you need device-work totals.
Stable serving permits resize, trim, adapter, and training work through writer-priority accelerator ownership. Their attribution remains null when no such operation overlapped the request's relevant boundary.
Understand stall reasons
For each inter-token gap, Kiln chooses the largest measured phase candidate.
The candidate must explain at least the smaller of 250 ms or half of the gap;
otherwise the reason is unexplained.
The closed reason set is:
actor_queue, actor_admission, actor_prefill, actor_decode,
actor_cycle_idle, response_delivery, handler_queue, client_delivery,
sampling, readback, gpu_lock_wait, graph_capture, graph_replay,
synchronization, resize, trim, adapter, training, unexplained
unexplained means the current instrumentation did not cover enough of the
gap. It is not evidence by itself of allocator activity, VRAM rebalancing, a
driver pause, or a particular backend defect.
Read per-token SSE events
With explicit request opt-in, a real-model stream emits a Kiln-specific object after the corresponding content enters the response-body channel:
{
"object": "kiln.token_timing",
"source": "batching_engine",
"token_index": 7,
"token_id": 4242,
"ready_ms": 12.0,
"producer_delivered_ms": 14.0,
"handler_received_ms": 17.0,
"body_enqueued_ms": 20.0,
"response_delivery_ms": 2.0,
"handler_queue_ms": 3.0,
"queue_delay_ms": 5.0,
"client_delivery_ms": 3.0,
"blocking_phase": "actor_prefill",
"blocking_phase_ms": 280.0
}
token_index is zero-based. token_id is the accepted model token represented
by the row, including a special token that decodes to no visible text. The
event does not include log probabilities or alternative candidates.
blocking_phase and blocking_phase_ms are null for the first token because
there is no preceding gap. They can also be null when no gap observation
exists.
Use the rolling decode endpoint
GET /v1/stats/decode summarizes request-local gaps retained during the last
60 seconds:
sample_count: gaps in the active rolling window;tok_per_sec: gap count divided by the sum of those gap durations;p50_itl_ms,p99_itl_ms,p999_itl_ms,mean_itl_ms, andmax_itl_ms;stall_threshold_ms:max(250 ms, 5 × p50 ITL); andstall_count,unexplained_stall_count, andstall_reasons.
This tok_per_sec value is a decode-cadence diagnostic. It excludes TTFT,
prompt prefill before the first token, queueing outside the sampled gaps, and
request-window concurrency. Do not compare it with end-to-end output
throughput from a serving benchmark. When the rolling window is empty, counts
and numeric summaries are zero.
Query Prometheus
GET /metrics exports the request timing families:
| Metric | Type | Population |
|---|---|---|
kiln_request_ttft_seconds |
histogram | Request receipt to first ready token |
kiln_token_itl_seconds |
histogram | Every retained request-local token gap |
kiln_request_latency_phase_seconds{phase} |
histogram | Non-null request phase observations |
kiln_token_stalls_total{reason} |
counter | Request-local gaps at or above the fixed 250 ms floor |
The histogram buckets are 5 ms, 10 ms, 25 ms, 50 ms, 100 ms, 250 ms, 500 ms,
1 s, 2.5 s, 5 s, 10 s, 30 s, 60 s, and +Inf. Labels use closed phase and
reason sets. Request IDs, prompts, model names, adapter names, client
identities, and error text are never labels.
Prometheus uses a fixed 250 ms stall definition so the counter remains comparable as workload mix changes. Request summaries and the rolling endpoint use the adaptive threshold. Account for that difference before comparing their counts.
histogram_quantile(
0.999,
sum by (le) (rate(kiln_token_itl_seconds_bucket[5m]))
)
sum by (reason) (rate(kiln_token_stalls_total[5m]))
histogram_quantile(
0.99,
sum by (phase, le) (
rate(kiln_request_latency_phase_seconds_bucket[5m])
)
)
Corroborate backend attribution
Request phases tell you which work overlapped a request. Health, trusted debug state, and backend metric families tell you what the process and accelerator were doing.
- Compare
gpu_lock_wait_mswith typed resize, trim, training, and adapter overlap. The narrower values sit inside the broader lock wait; do not add them together. - Treat
readback_msas an owned host-read boundary. Because accelerator work can be asynchronous, it can include waiting for earlier work on the same stream; it is not pure DMA time. - Use CUDA and ROCm synchronization counters to identify typed host waits. Never infer request time by subtracting unlocked process-global snapshots.
- Use graph lifetime telemetry for capture, replay, cache, and retained-memory totals. Request graph phases explain overlap, not total device work.
- On Vulkan, inspect the effective capability gauges. Cross-request prefix
reuse is currently correctness-quarantined on Vulkan, so exact repeats pay
fresh prefill. Resident prefill is disabled by
stableand admitted underexperimentalonly when backend and request checks pass.
Hardware receipts and host names are evidence about specific benchmark runs. They do not alter these metric definitions, enable a route, or become machine-specific dispatch policy.
Diagnose a pause
- Classify the symptom as slow TTFT, slow ITL, or slow end-to-end completion. Do not substitute one metric for another.
- Open the affected request in Latency diagnosis or capture its terminal metadata. Confirm the pause is request-local.
- Inspect the dominant stall reason and its duration. Preserve null, zero, and positive phase values.
- Corroborate that reason with the matching actor, synchronization, graph, allocator, mutation, or training telemetry over the same interval.
- Treat
unexplainedas an instrumentation gap. Gather more evidence before assigning a backend cause. - Reproduce with a source-bound workload. Change one configuration field per comparison arm, compare like-for-like metrics, and retain failed evidence.
The machine-readable field authorities are the inference schema and observability schema.