vLLM Explained

Serving metrics: TTFT, TPOT, ITL, throughput, goodput

BeginnerLearn the basicsabout 10 min read + 12 min deep dives

Code version: vLLM @ 3e44c0d · 2026-10-22

In one sentence

Serving metrics measure how long one user waits for tokens, and how many tokens and good requests the server makes per second.

The idea#

The timeline of one request#

A chat request goes through the same events every time. A client sends it. The server turns the messages into tokens. The request waits in a queue. Then the engine runs the prefill of the prompt and makes the first output token (Prefill and decode). After that, each step adds one more token. The request ends with the last token.

Each latency metric is the time between two of these events:

  • Time to first token (TTFT): from arrival to the first output token. The user sees nothing before it. Queue time and prefill time are both inside TTFT.
  • Time per output token (TPOT): the time from the first to the last token, divided by the number of tokens after the first. It is one number per request.
  • Inter-token latency (ITL): the time between two outputs that follow each other. A request with 101 output tokens gives 100 ITL samples.
  • End-to-end latency (E2E latency): from arrival to the last token. E2E = TTFT + TPOT × (output tokens − 1).

TPOT and ITL look the same, but they are not. TPOT is an average for one request. ITL keeps every gap, so one long gap stays visible. A long gap is a stall: for example, a step that also runs a long prefill of another request.

Many requests: throughput, concurrency, goodput#

A server has many users at the same time. Three numbers describe all of them together:

  • Request throughput: finished requests per second. Token throughput: output tokens per second (or input plus output tokens per second).
  • Concurrency: the number of requests in the server at one time. More concurrency gives bigger batches.
  • Goodput: requests per second that meet every SLO (service level objective). An SLO is a limit, for example "TTFT ≤ 500 ms and TPOT ≤ 50 ms".

Throughput alone can hide bad service. A server can finish many requests per second while half of them wait for 10 seconds. Goodput counts only the requests that the users find fast enough.

Percentiles: p50 and p99#

A mean hides the slow requests. A percentile does not. The p50 (median) is the value that half of the samples are below. The p99 is the value that 99 % of the samples are below. SLOs often use p99, because 1 in 100 samples is at or above the p99 value. With 1,000 requests, that is 10 slow requests.

A worked example. One request has 100 ITL gaps. 99 gaps are 18 ms and one gap is 218 ms (a stall). The mean ITL is (99 × 18 + 218) / 100 = 20 ms, and TPOT is also 20 ms. The p50 is 18 ms. The maximum is 218 ms. One stall changes the mean only by 2 ms, but the user sees a pause of 0.2 s.

The latency and throughput trade-off#

Batching puts the tokens of many requests into one step. The model reads its weights once for the whole batch (Batching). So a bigger batch gives many more tokens per second, but each step takes a little longer. Each step is one ITL gap for every request in the batch.

A worked example (illustrative numbers). Assume that a decode step takes 15 ms plus 0.1 ms per request in the batch.

Requests in the batchStep time = ITLOutput tokens per second
115.1 ms66
3218.2 ms1,758
12827.8 ms4,604
25640.6 ms6,305

From 1 to 256 requests, throughput grows by about 95 times. ITL grows only by about 2.7 times. With the SLO "TPOT ≤ 50 ms", all four batch sizes are good, so the largest batch gives the most goodput. With "TPOT ≤ 20 ms", only batches of up to 50 requests meet the SLO.

New requests also move the trade-off. A prefill chunk in a step makes that step longer for every running request. A bigger token budget gives a shorter TTFT for long prompts, but longer ITL stalls for the others.

An everyday comparison#

Think of a restaurant. TTFT is the time until the first dish comes to the table. ITL is the time between two dishes. Throughput is the number of guests that the kitchen serves in one hour. Goodput counts only the guests who got every dish in time. A cook can make 20 plates of the same dish at once. This serves more guests per hour, but each guest waits a little longer for each dish.

The comparison stops here: a kitchen cooks each dish only once. An LLM server must read all model weights again for each step. That is why a bigger batch costs so little extra time.

The picture#

Figure: each latency metric is the time between two events of one request (panel 1). Three processes measure them with three clocks (panel 2). Goodput counts only the requests that meet every SLO (panel 3). All numbers are a made-up example. Click the diagram to zoom.
Open full size ↗

Read panel 1 from left to right. The brackets above the line are the metrics that a user feels. The brackets below the line split TTFT into its parts. Panel 2 shows that vLLM subtracts only timestamps that come from the same process. The API server does the subtraction for the engine-core timestamps too. Panel 3 is the worked example of the deep dive "Worked example: five requests in one benchmark run".

In vLLM#

This is a basics page, so this part gives only pointers. The pages Metrics and observability: how to watch a vLLM server and Benchmarking vLLM: vllm bench serve, throughput, latency and startup explain the full code.

  1. The engine core marks the events. When statistics are on, the scheduler records a QUEUED event when a request enters it (vllm/v1/core/sched/scheduler.py:2746-2747). It records SCHEDULED with one timestamp per schedule() call (vllm/v1/core/sched/scheduler.py:664, vllm/v1/core/sched/scheduler.py:1420-1422). Every EngineCoreOutputs carries one time.monotonic() timestamp for its new tokens (vllm/v1/engine/__init__.py:296-297).
  2. The front end turns the events into metrics. IterationStats in the front end computes TTFT at the first output of a request and one ITL sample at each later output. It computes E2E, queue, prefill, decode and TPOT when a request finishes. The PrometheusStatLogger puts them into histograms, for example vllm:time_to_first_token_seconds (vllm/v1/metrics/loggers.py:930-935).
  3. The benchmark measures at the client. vllm bench serve sends streamed requests and times the SSE chunks (vllm/benchmarks/lib/endpoint_request_func.py:466-486). Then calculate_metrics() reports throughput, goodput, mean, median and percentiles (vllm/benchmarks/serve.py:602-812).
    def update_from_output(
        self,
        output: "EngineCoreOutput",
        engine_core_timestamp: float,
        is_prefilling: bool,
        req_stats: RequestStateStats,
        lora_states: "LoRARequestStates",
        lora_name: str | None,
    ):
        num_new_generation_tokens = len(output.new_token_ids)

        self.num_generation_tokens += num_new_generation_tokens
        if is_prefilling:
            if output.prefill_stats is not None:
                self.prompt_token_stats.update_from_output(output.prefill_stats)

            first_token_latency = self._time_since(req_stats.arrival_time)
            self.time_to_first_tokens_iter.append(first_token_latency)
            req_stats.first_token_latency = first_token_latency

        req_stats.num_generation_tokens += num_new_generation_tokens

        # Track if this request is corrupted (only check once per request)
        # Early exit if already marked as corrupted to avoid redundant checks
        if (
            self.compute_nans_in_logits
            and not req_stats.is_corrupted
            and output.num_nans_in_logits > 0
        ):
            req_stats.is_corrupted = True

        # Process request-level engine core events
        if output.events is not None:
            self.update_from_events(
                output.request_id,
                output.events,
                is_prefilling,
                req_stats,
                lora_states,
                lora_name,
            )

        # Process the batch-level "new tokens" engine core event
        if is_prefilling:
            req_stats.first_token_ts = engine_core_timestamp
        else:
            itl = engine_core_timestamp - req_stats.last_token_ts
            self.inter_token_latencies_iter.append(itl)

        req_stats.last_token_ts = engine_core_timestamp
IterationStats.update_from_output() runs once per request per engine output. TTFT uses the front-end clock and arrival_time. ITL uses the engine-core timestamps of two outputs that follow each other.

Deep dive#

The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.

Deep dive

Which clock, which process: where each timestamp comes from

vLLM computes an interval only from two timestamps of the same process. The design document gives the reason: monotonic clocks of two processes have different start points (docs/design/metrics.md:177-182).

TimestampProcessClockSet where
arrival_timeAPI servertime.time()first line of render_chat_async(), before the chat template and tokenization (vllm/renderers/base.py:1380); copied into the engine input (vllm/renderers/base.py:1274)
queued_tsengine coretime.monotonic()Scheduler.add_request() (vllm/v1/core/sched/scheduler.py:2746-2747, vllm/v1/engine/__init__.py:196-200)
scheduled_tsengine coretime.monotonic()one value at the start of schedule() for all new requests of the step (vllm/v1/core/sched/scheduler.py:664)
output timestampengine coretime.monotonic()when EngineCoreOutputs is made, after the step (vllm/v1/engine/__init__.py:296-297). It becomes first_token_ts and last_token_ts (vllm/v1/metrics/stats.py:551-558).
iteration_timestampAPI servertime.time()when the output handler gets one batch of outputs from the engine core (vllm/v1/engine/async_llm.py:879-885, vllm/v1/metrics/stats.py:477-478)

So TTFT and E2E latency are front-end intervals: iteration_timestamp − arrival_time (vllm/v1/metrics/stats.py:505-507). They include the chat template, tokenization, the trip to the engine core and back, and the wait. They do not include HTTP parsing before rendering, detokenization after the output arrives, or the network to the client. Queue, prefill, decode, ITL and TPOT are engine-core intervals.

The design document says: "Currently arrival_time starts when tokenization begins." (docs/design/metrics.md:262-263). In the code, it starts a little earlier: before the chat template (render_messages_async, vllm/renderers/base.py:1385-1388).

Chunked prefill. The scheduler sends an output for a request only when it has new tokens, a pooling output or a stop (vllm/v1/core/sched/scheduler.py:2293-2295). A prompt chunk that makes no token sends nothing. So the first output, and the end of TTFT, comes after the last chunk of the prompt.

Deep dive

How vLLM computes each interval, line by line

When a request finishes, the output processor calls update_from_finished_request() (vllm/v1/engine/output_processor.py:885-887):

    def update_from_finished_request(
        self,
        finish_reason: "FinishReason",
        request_id: str,
        num_prompt_tokens: int,
        max_tokens_param: int | None,
        req_stats: RequestStateStats,
        num_cached_tokens: int = 0,
    ):
        e2e_latency = self._time_since(req_stats.arrival_time)

        # Queued interval is from first QUEUED event to first SCHEDULED
        queued_time = req_stats.scheduled_ts - req_stats.queued_ts

        # Prefill interval is from first SCHEDULED to first NEW_TOKEN
        # Any preemptions during prefill is included in the interval
        prefill_time = req_stats.first_token_ts - req_stats.scheduled_ts

        # Decode interval is from first NEW_TOKEN to last NEW_TOKEN
        # Any preemptions during decode are included
        decode_time = req_stats.last_token_ts - req_stats.first_token_ts

        # Inference interval is from first SCHEDULED to last NEW_TOKEN
        # Any preemptions during prefill or decode are included
        inference_time = req_stats.last_token_ts - req_stats.scheduled_ts

        # Do not count the token generated by the prefill phase
        mean_time_per_output_token = (
            decode_time / (req_stats.num_generation_tokens - 1)
            if req_stats.num_generation_tokens - 1 > 0
            else 0
        )
  1. Line 594. E2E latency = front-end time now − arrival_time.
  2. Line 597. Queue time = first SCHEDULED − QUEUED.
  3. Line 601. Prefill time = first token − first SCHEDULED. A preemption during prefill stays inside this interval (comment on line 600).
  4. Line 605. Decode time = last token − first token.
  5. Line 609. Inference time = last token − first SCHEDULED.
  6. Lines 612-616. TPOT = decode time / (generated tokens − 1). The first token comes from prefill, so it is not counted. A request with 0 or 1 output token gets TPOT 0.

Docs and code differ on TPOT and ITL. The design document gives TPOT as (E2E latency − TTFT) / (output tokens − 1) (docs/design/metrics.md:55). The code divides the decode interval instead (lines 605 and 612-616). The document also calls each ITL sample a "wall-clock gap" between streamed outputs (docs/design/metrics.md:69-70). The code uses the monotonic timestamps of two engine outputs (vllm/v1/metrics/stats.py:554-556).

Why "first" SCHEDULED. A preempted request is scheduled again later. The code keeps only the first value: if req_stats.scheduled_ts == 0.0: # ignore preemptions (vllm/v1/metrics/stats.py:576-578). The design document instead says "between QUEUED and most recent SCHEDULED" (docs/design/metrics.md:212). The code wins: queue time does not grow after a preemption, but prefill or decode time does.

A preemption frees all blocks of the request and sets num_computed_tokens to 0. The request goes back to the front of the waiting queue (vllm/v1/core/sched/scheduler.py:1676-1700). It records a PREEMPTED event, which counts in vllm:num_preemptions (vllm/v1/metrics/stats.py:580-583).

Worked example (the request of the diagram). arrival_time = 0, queued_ts = 5 ms, scheduled_ts = 55 ms, first token = 200 ms, last token = 2,200 ms, 101 output tokens. Then queue = 50 ms, prefill = 145 ms and TTFT = 200 ms. Decode = 2,000 ms, inference = 2,145 ms, TPOT = 2,000 / 100 = 20 ms and E2E = 2,200 ms.

Deep dive

The Prometheus histograms: names, samples and buckets

The API server serves Prometheus metrics at /metrics (vllm/entrypoints/serve/instrumentator/metrics.py:77). A Prometheus histogram counts samples in buckets. Each name gets the series _bucket, _sum and _count.

MetricOne sample isObserved atBucket family
vllm:time_to_first_token_secondsTTFT of one requestvllm/v1/metrics/loggers.py:1284-1285time_to_first_token
vllm:inter_token_latency_secondsone gap between two outputsvllm/v1/metrics/loggers.py:1286-1287inter_token_latency
vllm:request_time_per_output_token_secondsTPOT of one finished requestvllm/v1/metrics/loggers.py:1324-1326inter_token_latency
vllm:e2e_request_latency_secondsE2E of one finished requestvllm/v1/metrics/loggers.py:1293-1295request_latency
vllm:request_queue_time_seconds, _prefill_time_, _decode_time_, _inference_time_one interval of one finished requestvllm/v1/metrics/loggers.py:1296-1307request_latency

The bucket families are in one file (vllm/v1/metrics/loggers.py:831-839). The TTFT buckets go from 1 ms to 2,560 s (vllm/v1/metrics/buckets.py:57-80). The ITL buckets go from 10 ms to 80 s (vllm/v1/metrics/buckets.py:84-104). The request-latency buckets go from 0.3 s to 7,680 s (vllm/v1/metrics/buckets.py:30-52). You can replace a family with the config field custom_histogram_buckets (vllm/config/observability.py:70, vllm/v1/metrics/buckets.py:232-233).

Why buckets matter. Prometheus estimates a percentile from the bucket counts. With the default ITL buckets, all gaps between 10 ms and 25 ms fall into one bucket. So a p99 ITL from Prometheus is only as exact as the bucket edges.

Throughput counters and the log line. The counters vllm:prompt_tokens and vllm:generation_tokens grow by the tokens of each output (vllm/v1/metrics/loggers.py:1262-1272). The LoggingStatLogger also writes a log line with "Avg prompt throughput" and "Avg generation throughput" (vllm/v1/metrics/loggers.py:283-284). It divides the tokens by the time since the last log (vllm/v1/metrics/loggers.py:169-174). The API server writes this line every VLLM_LOG_STATS_INTERVAL seconds, default 10 (vllm/envs.py:874-878, vllm/entrypoints/launchers/utils/server_utils.py:79-84).

Turn off. The API server gives --disable-log-stats to AsyncLLM (vllm/entrypoints/launchers/api_server/entry.py:99). Then AsyncLLM makes no default stat loggers. A stat-logger plugin still turns statistics on, but only for the plugin loggers (vllm/v1/engine/async_llm.py:158-159, vllm/v1/engine/async_llm.py:204-210).

Deep dive

vllm bench serve: how the client measures

vllm bench serve is a subcommand that calls vllm/benchmarks/serve.py (vllm/entrypoints/cli/benchmark/serve.py:10-21). It sends requests to a running server and times them at the client.

One request. For the chat API, the client sends "stream": true with usage in the last chunk (vllm/benchmarks/lib/endpoint_request_func.py:423-432). It starts a time.perf_counter() clock before the POST (vllm/benchmarks/lib/endpoint_request_func.py:443-445). Then:

  1. TTFT = the time of the first SSE chunk with output − start (line 477).
  2. Each later chunk with output adds one ITL sample: the gap to the previous chunk with output (lines 481, 486).
  3. The usage chunk gives the output token count, but it does not move the end time (lines 484-490).
  4. Latency (E2EL) = the time of the last output chunk − start (vllm/benchmarks/lib/endpoint_request_func.py:501).

All requests. calculate_metrics() computes (vllm/benchmarks/serve.py:654-660, vllm/benchmarks/serve.py:778-781):

  • TPOT per request = (latency − TTFT) / (output tokens − 1). Requests with 1 output token are not in the TPOT statistics.
  • Request throughput = completed / duration. Output token throughput = output tokens / duration. Total token throughput = (input + output tokens) / duration.
  • The duration is the wall time of the whole run, from the start of the benchmark to the last result (vllm/benchmarks/serve.py:1048, vllm/benchmarks/serve.py:1123).
  • Mean, median, standard deviation and the selected percentiles, with np.percentile (vllm/benchmarks/serve.py:782-806).

Goodput. A request is good when every given SLO value is greater than or equal to its measured value (vllm/benchmarks/serve.py:669-692). Request goodput = good requests / duration. A request with 1 output token has TPOT 0 for this check.

    if goodput_config_dict:
        valid_metrics = []
        slo_values = []

        if "ttft" in goodput_config_dict:
            valid_metrics.append(ttfts)
            slo_values.append(
                goodput_config_dict["ttft"] / MILLISECONDS_TO_SECONDS_CONVERSION
            )
        if "tpot" in goodput_config_dict:
            valid_metrics.append(all_tpots)
            slo_values.append(
                goodput_config_dict["tpot"] / MILLISECONDS_TO_SECONDS_CONVERSION
            )
        if "e2el" in goodput_config_dict:
            valid_metrics.append(e2els)
            slo_values.append(
                goodput_config_dict["e2el"] / MILLISECONDS_TO_SECONDS_CONVERSION
            )

        for req_metric in zip(*valid_metrics):
            is_good_req = all([s >= r for s, r in zip(slo_values, req_metric)])
            if is_good_req:
                good_completed += 1

The main flags.

FlagDefaultEffect
--request-rateinfRequests per second. inf sends all requests at time 0. Otherwise the arrivals follow a Poisson process or a gamma distribution (vllm/benchmarks/serve.py:1745-1753).
--burstiness1.01 = Poisson. Below 1 = more bursts, above 1 = more even arrivals (vllm/benchmarks/serve.py:1754-1764).
--max-concurrencynoneA semaphore that limits requests in flight. The wait for it is reported as client queue time, not as TTFT (vllm/benchmarks/serve.py:1678-1690, vllm/benchmarks/serve.py:1001-1016).
--percentile-metricsttft,tpot,itl (generation)Which metrics get percentiles. e2el and the client-queue metrics are also allowed. The default for pooling models is e2el (vllm/benchmarks/serve.py:1847-1857, vllm/benchmarks/serve.py:2255-2263).
--metric-percentiles99Which percentiles, for example 50,90,99 (vllm/benchmarks/serve.py:1858-1866).
--goodputnoneSLOs as KEY:VALUE in ms; keys ttft, tpot, e2el (vllm/benchmarks/serve.py:1867-1878).

An example command (not in the repo):

vllm bench serve --model meta-llama/Llama-3.1-8B-Instruct \
  --request-rate 4 --num-prompts 200 \
  --metric-percentiles 50,99 --goodput ttft:500 tpot:50
Deep dive

Worked example: five requests in one benchmark run

This is a made-up workload (panel 3 of the diagram). The run lasts 8.0 s. The SLO is --goodput ttft:500 tpot:50.

RequestInput tokensOutput tokensTTFTTPOTE2EGood?
A1,000101200 ms20 ms2,200 msyes
B50051150 ms25 ms1,400 msyes
C2,000201650 ms22 ms5,050 msno (TTFT)
D800101300 ms55 ms5,800 msno (TPOT)
E3001120 ms(0 for goodput)120 msyes

Check one row: E2E of A = 200 + (101 − 1) × 20 = 2,200 ms.

  • Request throughput = 5 / 8.0 = 0.625 req/s.
  • Output token throughput = 455 / 8.0 = 56.9 tok/s. Total token throughput = (4,600 + 455) / 8.0 = 631.9 tok/s.
  • Request goodput = 3 / 8.0 = 0.375 req/s. Throughput says 0.625, but only 60 % of the requests met the SLO.
  • TTFT: sorted 120, 150, 200, 300, 650. Mean 284 ms, median 200 ms. np.percentile interpolates in a straight line by default. The p99 is at index 0.99 × 4 = 3.96, so p99 = 300 + 0.96 × (650 − 300) = 636 ms.
  • TPOT: E has 1 output token, so only A to D count: 20, 22, 25, 55. Mean 30.5 ms, median 23.5 ms. The p99 is at index 0.99 × 3 = 2.97, so p99 = 25 + 0.97 × (55 − 25) = 54.1 ms.
  • Average concurrency = the sum of all E2E times / duration = 14.57 s / 8.0 s = 1.8 requests in flight.

The mean TTFT (284 ms) is below the SLO, but one request in five missed it. This is why SLOs use percentiles or goodput, not means.

Deep dive

Batching knobs in vLLM that move the trade-off

The vLLM scheduler has no prefill phase and no decode phase. Each step gives tokens to requests until their computed tokens catch up with their known tokens (vllm/v1/core/sched/scheduler.py:624-633). So one step can hold decode tokens and prefill chunks together.

  1. The step starts with the token budget max_num_scheduled_tokens (vllm/v1/core/sched/scheduler.py:642). This is normally max_num_batched_tokens (vllm/v1/core/sched/scheduler.py:138-142).
  2. It first serves the running requests (vllm/v1/core/sched/scheduler.py:713-715).
  3. It then takes waiting requests with the rest of the budget (vllm/v1/core/sched/scheduler.py:956). Chunked prefill is on by default for generative models that are not encoder-decoder models (vllm/engine/arg_utils.py:3031-3035). So a long prompt takes only the rest of the budget.

Defaults for vllm serve (usage context OPENAI_API_SERVER) on a GPU depend on the GPU memory (vllm/engine/arg_utils.py:2960-2993). They apply only when you do not set the flag. --performance-mode throughput doubles both values (vllm/engine/arg_utils.py:3171-3175). TPU and CPU platforms have other values (vllm/engine/arg_utils.py:2995-3024).

GPUmax_num_batched_tokensmax_num_seqs
160 GiB or more (for example B200)16,3841,024
70 GiB or more, not A100, not GB10 (for example H100)8,1921,024
other GPUs2,048256

The vLLM optimization guide gives the direction. A smaller max_num_batched_tokens (for example 2,048) gives a better ITL, and a larger one gives a better TTFT (docs/configuration/optimization.md:64-65). max_num_seqs limits the requests that run at the same time, so it limits the batch and the concurrency inside the engine (vllm/v1/core/sched/scheduler.py:130-137). The page Tuning vLLM: throughput and latency knobs, their real defaults and what each one does explains both with the code.

Deep dive

Per-request metrics in the API response

With --enable-per-request-metrics (default off, vllm/entrypoints/common/cli_args.py:150-151), the response has a metrics object. In streaming, it comes in the final usage chunk. The server sends that chunk only when usage is on: stream_options.include_usage or --enable-force-include-usage. The server leaves the object out when n is more than 1 (vllm/entrypoints/openai/chat_completion/serving.py:873-885). The flag needs engine statistics: with --disable-log-stats, start-up fails (vllm/entrypoints/common/cli_args.py:536-542).

The other fields: queue_time_ms = scheduled_ts − queued_ts; generation_time_ms = decode interval; mean_itl_ms = decode interval / (tokens − 1); tokens_per_second = all output tokens / inference interval (vllm/entrypoints/generate/base/serving.py:76-89). vllm bench serve reads queue_time_ms and time_to_first_token_ms from this object when the server sends it (vllm/benchmarks/lib/endpoint_request_func.py:110-125).

What can go wrong#

Why it matters. Every tuning choice in vLLM moves TTFT, ITL and throughput against each other. You can only tune well when you know what each number measures, where it is measured, and which SLO your users need.

Check yourself#

  1. 1
    A request has TTFT 300 ms, 51 output tokens and E2E latency 1,300 ms. What is its TPOT?
    easy
    Show answer

    TPOT = (E2E − TTFT) / (output tokens − 1) = (1,300 − 300) / 50 = 20 ms. This is the formula of vllm bench serve (vllm/benchmarks/serve.py:655-657). The Prometheus TPOT uses the engine-core decode interval instead of E2E − TTFT. It also does not count the first token, because the first token comes from prefill (vllm/v1/metrics/stats.py:611-616).

  2. 2
    A run finishes 100 requests in 20 s. 80 of them meet every SLO. What are the request throughput and the request goodput?
    easy
    Show answer

    Throughput = 100 / 20 = 5 req/s. Goodput = 80 / 20 = 4 req/s (vllm/benchmarks/serve.py:778-779).

  3. 3
    Why does vLLM take the queue time from engine-core timestamps, and not from API server timestamps?
    medium
    Show answer

    Only the engine core sees the QUEUED and SCHEDULED events. An interval must use two timestamps of the same process, because monotonic clocks of two processes have different start points (docs/design/metrics.md:177-182). So the engine core records both events with time.monotonic() (vllm/v1/engine/__init__.py:196-200), and the front end subtracts them (vllm/v1/metrics/stats.py:597).

  4. 4
    A request waits 2 s, runs its prefill, is preempted during decode, waits 1 s more, and finishes. Which Prometheus interval gets the extra 1 s: queue time, prefill time or decode time?
    hard
    Show answer

    Decode time (and inference time). The code keeps only the first SCHEDULED timestamp (vllm/v1/metrics/stats.py:577-578). So queue time stays 2 s. Decode time = last token − first token (vllm/v1/metrics/stats.py:605), and the extra wait is between these two tokens. The ITL histogram also gets one long gap.

  5. 5
    You enable --enable-per-request-metrics. A response says time_to_first_token_ms = 120, but your Prometheus TTFT for the same request is about 900 ms. Give a reason from the code.
    hard
    Show answer

    The response field is first token − scheduled_ts (vllm/entrypoints/generate/base/serving.py:73-74): the prefill interval only. The histogram TTFT is front-end time − arrival_time (vllm/v1/metrics/stats.py:525), and arrival_time is set before the chat template (vllm/renderers/base.py:1380). So the difference of about 780 ms is the time before scheduled_ts (rendering, the trip to the engine core, the queue). It also has the trip of the first output back to the front end. Compare it with queue_time_ms in the same response to see the queue part.