Metrics Reference¶
Complete reference for the Prometheus metrics SMG exports. Metrics are grouped by layer, from the gateway's own runtime through HTTP, routing, workers, discovery, and MCP, followed by groups that belong to specific features: routing policies, the priority scheduler, the RL control plane, and the HA mesh.
A series appears on /metrics only after the first event that records it, and several groups exist only when the feature that produces them is enabled. Each entry says when it is emitted.
Metrics Endpoint¶
Metrics are served at GET /metrics on a dedicated listener, --prometheus-host:--prometheus-port (default 0.0.0.0:29000):
curl http://localhost:29000/metricsConfigure via CLI:
smg launch --prometheus-port 29000 --prometheus-host 0.0.0.0- The port must be greater than
0. Although--helpdescribes--prometheus-port 0as binding an OS-assigned port, configuration validation rejects it at startup (Port must be > 0). - The listener binds at startup and logs
Metrics server listening on <address> (/metrics). If the port is already taken, startup fails withfailed to bind metrics server on <address>instead of continuing without metrics. --prometheus-duration-bucketschanges the buckets of the latency histograms (see Histogram Buckets).
Engine Metrics Passthrough¶
GET /engine_metrics returns the engines' own Prometheus metrics in a single exposition. It is served on the main API listener (--host:--port, default port 30000), not on the metrics port, and like /health it requires no API key.
curl http://localhost:30000/engine_metrics- SMG fetches
<worker URL>/metricsfrom every registered worker, once per base URL (data-parallel ranks that share a URL are scraped once), and adds aworker_addrlabel holding the worker's base URL to every sample. - Metric names, HELP text, and label values are passed through unchanged, colons included (
vllm:num_requests_running,sglang:...), so queries written against an engine's own/metricswork against the gateway. Before v1.11.0 the colons were rewritten to underscores (smg-project/smg#2636). - Workers whose scrape fails or returns a non-success status are left out. If no worker is registered, or every scrape fails, the endpoint returns
500with a plain-text reason (No available workersorAll backend requests failed).
Label Cardinality¶
Labels whose values a client can influence are bounded, so unexpected input cannot create an unbounded number of series:
| Label | Bound |
|---|---|
path (HTTP metrics) |
The matched route template, for example /v1/responses/{response_id}. Requests that match no route get a bare 404 from a fallback outside the metrics layer and are not recorded. |
method (HTTP metrics) |
GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS; any other method is OTHER. |
model |
The first 1,024 distinct values keep their own series; every later new value is reported as other (smg-project/smg#2093). |
tool_name (MCP metrics) |
Same rule as model: 1,024 distinct values, then other. |
op (RL control plane) |
A fixed list of engine control operations; any other path is other. |
The model and tool_name caps never evict a value once it is admitted, so the set of series stops growing at the cap instead of churning. Worker URLs (worker), status codes, and gateway error codes come from the gateway itself and are not capped. The priority scheduler's tenant label is not capped either; see the Priority Scheduler Reference.
Removed workers¶
The exporter cannot delete a series, so when a worker is removed SMG overwrites its per-worker gauges instead: smg_worker_health and smg_worker_cb_state become -1, smg_worker_requests_active and the circuit-breaker streak gauges become 0, and the worker's smg_engine_* gauges become -1, as they also do while a worker is out of Ready. smg_worker_http2 keeps its last value. Filter on the value (for example == 1 or >= 0) when you aggregate these gauges.
Layer 0: Runtime Metrics¶
Health of the gateway process itself: the Tokio async runtime that serves requests, and the memory allocator.
Tokio Runtime Metrics¶
A background task started with the metrics server observes the runtime that serves requests. An event-loop canary sleeps for 10 ms in a loop and records how late it wakes up; a sampler reads the runtime's counters every second.
| Metric | Type | Labels | Description |
|---|---|---|---|
smg_tokio_event_loop_delay_seconds |
Histogram | None | How late the 10 ms canary sleep woke up |
smg_tokio_event_loop_stalls_total |
Counter | None | Canary wake-ups more than 5 ms late |
smg_tokio_global_queue_depth |
Gauge | None | Tasks waiting in the runtime's global queue |
smg_tokio_alive_tasks |
Gauge | None | Tasks spawned and not yet completed |
smg_tokio_workers |
Gauge | None | Runtime worker threads (see --runtime-worker-threads, a Rust-binary flag the pip smg launch does not accept) |
smg_tokio_worker_busy_ratio |
Gauge | worker |
Fraction of the last sampling interval each worker thread was busy (0.0-1.0) |
smg_tokio_worker_parks_total |
Counter | worker |
Times each worker thread parked (went idle) |
worker is the runtime worker index (0, 1, ...). The delay histogram has its own buckets: 0, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0 seconds.
A stall means every task on the runtime, including accept loops, health checks, and streaming responses, was delayed by at least the measured drift, so a rising stall rate separates "gateway starved" from "backend slow". Busy ratios near 1.0 together with a growing global queue mean the runtime is saturated; a near-zero park rate while busy means sustained overload rather than bursts.
# Event-loop stalls per second
rate(smg_tokio_event_loop_stalls_total[5m])
# P99 event-loop wake delay
histogram_quantile(0.99, sum by (le) (rate(smg_tokio_event_loop_delay_seconds_bucket[5m])))
# Mean worker-thread utilization
avg(smg_tokio_worker_busy_ratio)Allocator Metrics¶
SMG uses jemalloc as its global allocator and exports jemalloc's own accounting, refreshed every 60 seconds by a dedicated thread (smg-project/smg#2125). The smg binary and the Python package both export these gauges: they use jemalloc and build with the jemalloc-stats feature, which is on by default. Builds for musl or MSVC targets use the system allocator and do not export them.
| Metric | Type | Labels | Description |
|---|---|---|---|
smg_allocator_allocated_bytes |
Gauge | None | Bytes in live Rust allocations |
smg_allocator_active_bytes |
Gauge | None | Bytes in active pages |
smg_allocator_resident_bytes |
Gauge | None | Upper bound on resident bytes of the jemalloc heap |
smg_allocator_metadata_bytes |
Gauge | None | jemalloc metadata bytes |
Resident bytes that grow while allocated bytes stay flat point at memory the allocator retains; allocated bytes that grow with them point at data the gateway itself holds. The gauges cover only Rust allocations made through SMG's jemalloc instance: memory allocated by the Python interpreter or by bundled C libraries is not included.
# Memory the allocator holds beyond live allocations
smg_allocator_resident_bytes - smg_allocator_allocated_bytesLayer 1: HTTP Metrics¶
Metrics for requests arriving at the gateway's main listener. Probes served on the dedicated --health-check-port listener bypass this layer.
smg_http_requests_total¶
HTTP requests received, counted when the request arrives.
| Type | Labels |
|---|---|
| Counter | method, path |
# Request rate by endpoint
sum by (path) (rate(smg_http_requests_total[5m]))
# Total request rate
sum(rate(smg_http_requests_total[5m]))smg_http_request_duration_seconds¶
Time from receiving the request until the response head is returned. For a streaming response this does not include the time spent streaming the body.
| Type | Labels |
|---|---|
| Histogram | method, path |
# P99 latency by endpoint
histogram_quantile(0.99, sum by (path, le) (rate(smg_http_request_duration_seconds_bucket[5m])))
# Average latency
sum(rate(smg_http_request_duration_seconds_sum[5m])) / sum(rate(smg_http_request_duration_seconds_count[5m]))smg_http_responses_total¶
HTTP responses by path, status code, and gateway error code.
| Type | Labels |
|---|---|
| Counter | path, status_code, error_code |
error_code is the code carried by error responses the gateway generates (for example admission_queue_full or worker_overload_protection_shed); it is empty for other responses.
# Error rate (5xx responses)
sum(rate(smg_http_responses_total{status_code=~"5.."}[5m])) / sum(rate(smg_http_responses_total[5m]))
# Success rate
sum(rate(smg_http_responses_total{status_code="200"}[5m])) / sum(rate(smg_http_responses_total[5m]))
# Success rate for /v1/responses
sum(rate(smg_http_responses_total{path="/v1/responses",status_code=~"2.."}[5m]))
/
sum(rate(smg_http_responses_total{path="/v1/responses"}[5m]))
# Gateway-generated errors by code
sum by (error_code) (rate(smg_http_responses_total{error_code!=""}[5m]))smg_http_connections_active¶
Requests currently in flight on the main listener. A streaming response stays in flight until its body finishes or the client disconnects (smg-project/smg#2130). Despite the name, it counts requests, not TCP connections.
| Type | Labels |
|---|---|
| Gauge | None |
smg_http_inflight_request_age_count¶
In-flight requests per age bucket, sampled every 20 seconds, for spotting stuck requests and for heatmaps. Unlike Prometheus histogram buckets, these buckets are not cumulative: each series counts requests with gt < age <= le.
| Type | Labels |
|---|---|
| Gauge | gt, le |
Age bounds (seconds): 30, 60, 180, 300, 600, 1200, 3600, 7200, 14400, 28800, 86400. The first bucket has gt="0"; the last has gt="86400" and le="+Inf".
# Requests in flight for more than 10 minutes
sum(smg_http_inflight_request_age_count{gt=~"600|1200|3600|7200|14400|28800|86400"})smg_http_rate_limit_total¶
Admission decisions of the concurrency limiter.
| Type | Labels |
|---|---|
| Counter | result |
Values: allowed, rejected
Recorded when --max-concurrent-requests is set. With the priority scheduler enabled, only rejections by the --rate-limit-tokens-per-second limiter are counted here; the scheduler's own outcomes are in smg_scheduler_admit_total.
# Rejection ratio
sum(rate(smg_http_rate_limit_total{result="rejected"}[5m])) / sum(rate(smg_http_rate_limit_total[5m]))Admission Queue Metrics¶
When --max-concurrent-requests is set and the priority scheduler is off, every request to an inference route holds an admission token for its full lifetime, streaming body included. A request that finds no free token waits in a FIFO queue of --queue-size slots (default 100) for up to --queue-timeout-secs (default 60). These metrics expose that queue.
smg_admission_inflight¶
Requests currently holding an admission token. Compare it with --max-concurrent-requests to see how close the gateway is to its concurrency cap.
| Type | Labels |
|---|---|
| Gauge | None |
smg_admission_queue_depth¶
Requests currently waiting in the admission queue.
| Type | Labels |
|---|---|
| Gauge | None |
smg_admission_queue_rejected_total¶
Requests rejected at admission.
| Type | Labels |
|---|---|
| Counter | reason |
reason |
Response | Cause |
|---|---|---|
full |
429 admission_queue_full |
Every queue slot was taken |
timeout |
503 admission_queue_timeout |
The request waited --queue-timeout-secs without getting a token |
multimodal_too_large |
413 multimodal_payload_too_large |
The request's preprocessed media alone exceeds --multimodal-max-inflight-bytes |
multimodal_inflight |
429 multimodal_inflight_budget |
The gateway's in-flight media budget stayed full |
The two multimodal_* reasons come from the --multimodal-max-inflight-bytes budget and are recorded whether or not the concurrency limiter is on. With queuing disabled (--queue-size 0), a request that finds no token gets 429 admission_queue_full but is counted only in smg_http_rate_limit_total.
# Admission rejections per second, by reason
sum by (reason) (rate(smg_admission_queue_rejected_total[5m]))
# Requests waiting for a token
smg_admission_queue_depthLayer 2: Router Metrics¶
Metrics for request routing and processing.
smg_router_requests_total¶
Requests processed by the router. The gRPC router records every retry attempt as another request, and each failed attempt in smg_router_request_errors_total.
| Type | Labels |
|---|---|
| Counter | router_type, backend_type, connection_mode, model, endpoint, streaming |
router_type:http,grpc,openaibackend_type:regular,pd(prefill-decode and encode-prefill-decode),external. The streaming metrics below also useharmony, for Harmony-mode (gpt-oss) streaming chat on the gRPC router.connection_mode:http,grpc,websocket,webrtc. Requests to ZMQ workers run through the gRPC pipeline and are counted withrouter_type="grpc"andconnection_mode="grpc".endpoint:chat,generate,completions,responses,messages,embeddings,classify,rerank,audio_transcriptions,realtime,realtime_sessions,realtime_client_secrets,realtime_transcription, orotherfor routes without a dedicated labelstreaming:true,false
# Request rate by model
sum by (model) (rate(smg_router_requests_total[5m]))
# Streaming vs non-streaming
sum by (streaming) (rate(smg_router_requests_total[5m]))smg_router_request_duration_seconds¶
Total router request duration.
| Type | Labels |
|---|---|
| Histogram | router_type, backend_type, connection_mode, model, endpoint |
smg_router_request_errors_total¶
Router errors by type.
| Type | Labels |
|---|---|
| Counter | router_type, backend_type, connection_mode, model, endpoint, error_type |
Error types: no_workers, timeout, backend_error, validation_error, internal_error. The Anthropic Messages provider also reports parse_error and streaming_error.
# Error rate by type
sum by (error_type) (rate(smg_router_request_errors_total[5m]))smg_router_ttft_seconds¶
Time to first token, recorded for streaming responses on the gRPC router. For backend_type="pd" the clock starts when the decode request is sent; see smg_pd_ttft_seconds for the end-to-end value.
| Type | Labels |
|---|---|
| Histogram | router_type, backend_type, model, endpoint |
# P50 TTFT by model
histogram_quantile(0.5, sum by (model, le) (rate(smg_router_ttft_seconds_bucket[5m])))smg_router_tpot_seconds¶
Time per output token (the mean inter-token latency of a request), recorded for streaming responses on the gRPC router that produce more than one token.
| Type | Labels |
|---|---|
| Histogram | router_type, backend_type, model, endpoint |
# Average TPOT
sum(rate(smg_router_tpot_seconds_sum[5m])) / sum(rate(smg_router_tpot_seconds_count[5m]))smg_router_tokens_total¶
Token counts by type. Recorded for streaming responses on the gRPC router, and for non-streaming responses from the Anthropic Messages provider.
| Type | Labels |
|---|---|
| Counter | router_type, backend_type, model, endpoint, token_type |
Token types: input, output
# Tokens per second
sum by (token_type) (rate(smg_router_tokens_total[5m]))
# Output/input ratio
sum(rate(smg_router_tokens_total{token_type="output"}[5m])) / sum(rate(smg_router_tokens_total{token_type="input"}[5m]))smg_router_generation_duration_seconds¶
Total generation time of a streaming response on the gRPC router.
| Type | Labels |
|---|---|
| Histogram | router_type, backend_type, model, endpoint |
smg_router_upstream_responses_total¶
Responses from upstream workers, recorded by the HTTP router (router_type="http").
| Type | Labels |
|---|---|
| Counter | router_type, status_code, error_code |
smg_router_upstream_send_retries_total¶
Requests the HTTP router sent a second time because the first send failed before any response arrived, for example on a pooled connection the worker had already closed (smg-project/smg#2162). There is at most one such resend per send, it covers both regular and PD HTTP dispatch, and it does not apply to timeouts or to request bodies that cannot be replayed.
| Type | Labels |
|---|---|
| Counter | router_type |
smg_router_request_body_path_total¶
Per-request decision whether the router buffers the request body or streams it to the worker unread (smg-project/smg#2286). Recorded for /generate, /v1/chat/completions, /v1/completions, /v1/messages, /v1/embeddings, and /v1/classify. See Request Body Streaming.
| Type | Labels |
|---|---|
| Counter | path, reason |
path:buffered,streamedreasonforbuffered:routing_key_override,policy_needs_text,model_ambiguous,worker_mutates_body,wasm_request_hook,no_content_length,no_available_worker,model_selection,retryable(kept replayable for router retries, within--max-buffered-request-bytes), or the router family name (for examplepdorgrpc) when the active router always parses bodiesreasonforstreamed:retry_forfeited(larger than--max-buffered-request-bytes, so router retries are given up),pure_forward(router retries are off)
# Body-path decisions by reason
sum by (path, reason) (rate(smg_router_request_body_path_total[5m]))smg_router_request_buffers_released_early_bytes_total¶
Serialized size of request buffers the router freed as soon as the request was dispatched, instead of holding them until the response completed (smg-project/smg#2232). The HTTP routers release at dispatch when router retries are disabled; the gRPC router counts the built request's wire size when it drops the parsed request at dispatch.
| Type | Labels |
|---|---|
| Counter | None |
PD Disaggregation Metrics¶
Signals that only the gateway can measure, because it is the one component that sees both the prefill and the decode leg of a disaggregated request. Durations are recorded once per request, not per retry attempt. See PD Disaggregation.
The three histograms share the labels backend_type (always pd), model, and runtime, the worker's runtime type (for example sglang, vllm, or tokenspeed).
smg_pd_ttft_seconds¶
End-to-end time to first token of a disaggregated request, from the start of the prefill leg to the first decode token. It complements smg_router_ttft_seconds{backend_type="pd"}, which starts at the decode dispatch; for sequential PD the two differ by the prefill and KV-transfer time.
| Type | Labels |
|---|---|
| Histogram | backend_type, model, runtime |
smg_pd_prefill_duration_seconds¶
Duration of the prefill leg. Recorded by the HTTP PD router and for sequential PD on the gRPC router.
| Type | Labels |
|---|---|
| Histogram | backend_type, model, runtime |
smg_pd_kv_transfer_duration_seconds¶
KV-transfer window, from the prefill leg draining to the decode request being sent. Recorded for sequential (vLLM) PD on the gRPC router.
| Type | Labels |
|---|---|
| Histogram | backend_type, model, runtime |
smg_pd_kv_connector_mode_total¶
KV connector mode chosen for vLLM prefill-decode dispatches.
| Type | Labels |
|---|---|
| Counter | mode |
Modes: mooncake, nixl, passthrough
smg_pd_bootstrap_failures_total¶
HTTP PD requests whose body could not take the bootstrap fields (the body is not a JSON object).
| Type | Labels |
|---|---|
| Counter | None |
smg_pd_kv_transfer_failures_total¶
vLLM NIXL dispatches where the prefill leg returned no kv_transfer_params, so the decode worker recomputes the prompt itself.
| Type | Labels |
|---|---|
| Counter | None |
smg_pd_admission_waits_total¶
gRPC prefill-decode dispatches that waited for room in the decode engine's running window (its --max-num-seqs / --max-running-requests) before being sent (smg-project/smg#2466).
| Type | Labels |
|---|---|
| Counter | None |
smg_pd_admission_sheds_total¶
gRPC prefill-decode dispatches shed with 503 worker_overload_protection_shed, because no decode room freed within --pd-admission-wait-secs (default 30) or because the request needs more rooms than the window holds. Each shed also increments smg_worker_overload_shed_total{stage="pd_admission"}. Decode engines that report no running window are never gated.
| Type | Labels |
|---|---|
| Counter | None |
# P95 end-to-end TTFT for PD requests, by model
histogram_quantile(0.95, sum by (model, le) (rate(smg_pd_ttft_seconds_bucket[5m])))
# PD dispatches that waited for, or were shed by, decode admission
rate(smg_pd_admission_waits_total[5m])
rate(smg_pd_admission_sheds_total[5m])Multimodal Metrics¶
Recorded by the gRPC pipeline for requests that carry images, audio, or video. See Multimodal.
smg_mm_tensors_total¶
Preprocessed media tensors sent to engines, by transport.
| Type | Labels |
|---|---|
| Counter | runtime, path |
runtime:vllm,tokenspeedpath:inline(bytes in the request),shm(shared memory, per--multimodal-tensor-transport),remote(RDMA pixel lane)
smg_mm_tensor_bytes_total¶
Bytes of the tensors counted by smg_mm_tensors_total.
| Type | Labels |
|---|---|
| Counter | runtime, path |
smg_mm_shm_write_failures_total¶
Shared-memory tensor writes that failed and fell back to inline transport.
| Type | Labels |
|---|---|
| Counter | runtime |
smg_mm_processing_total¶
Where a multimodal request's media was fetched and preprocessed (the --mm-processing decision for vLLM gRPC workers), and why.
| Type | Labels |
|---|---|
| Counter | model, mode, reason |
mode:router,workerreason:config(set explicitly torouterorworker); underauto:auto_uniform(every worker of the model accepts media references),auto_mixed,auto_none(no registered worker),plan_not_forwardable,model_not_opted_in
Tokenizer Cache Metrics¶
Process-wide activity of the tokenizer L0/L1 caches, summed across all tokenizers (smg-project/smg#2603). The totals are read on every scrape, so both layers always appear; a disabled layer stays at zero. Labels carry no model IDs or prompt text.
smg_tokenizer_cache_lookups_total¶
Cache lookups by layer and result, one outcome per lookup. An L0 hit skips L1, and L1 counts inputs without usable special-token boundaries as misses.
| Type | Labels |
|---|---|
| Counter | layer, result |
layer:l0(exact match),l1(prefix match)result:hit,miss
smg_tokenizer_cache_evictions_total¶
Entries removed to make room. Clearing a cache, dropping a tokenizer, or replacing an entry does not count.
| Type | Labels |
|---|---|
| Counter | layer |
smg_tokenizer_cache_reused_bytes_total¶
UTF-8 input bytes served by cache hits: whole inputs for L0, matched prefixes for L1. This measures reuse volume, not cache memory or CPU time saved.
| Type | Labels |
|---|---|
| Counter | layer |
# Hit ratio per layer (each layer against its own lookups)
sum by (layer) (rate(smg_tokenizer_cache_lookups_total{result="hit"}[5m]))
/
sum by (layer) (rate(smg_tokenizer_cache_lookups_total[5m]))
# Evictions per second, a sign the cache is too small
sum by (layer) (rate(smg_tokenizer_cache_evictions_total[5m]))Layer 3: Worker Metrics¶
Metrics for worker pool management and resilience.
smg_worker_pool_size¶
Workers registered, per type, connection mode, and model.
| Type | Labels |
|---|---|
| Gauge | worker_type, connection_mode, model |
worker_type:regular,prefill,decode,encodeconnection_mode:http,grpc,zmq
smg_worker_requests_active¶
Requests the gateway currently has in flight to each worker.
| Type | Labels |
|---|---|
| Gauge | worker |
# Load distribution across workers
smg_worker_requests_active / ignoring(worker) group_left sum without (worker) (smg_worker_requests_active)smg_worker_health¶
Worker health status.
| Type | Labels | Values |
|---|---|---|
| Gauge | worker |
1 = Ready, 0 = any other status (pending, not ready, failed, or draining), -1 = removed |
# Count healthy workers
count(smg_worker_health == 1)
# Alert on unhealthy workers
smg_worker_health == 0smg_worker_http2¶
Whether the gateway speaks HTTP/2 with prior knowledge to each worker: 1 when it was negotiated at registration under --upstream-http2 or pinned with http_pool.http2 on the worker spec, otherwise 0 (HTTP/1.1). Set when the worker is registered.
| Type | Labels |
|---|---|
| Gauge | worker |
smg_worker_health_checks_total¶
Health check results.
| Type | Labels |
|---|---|
| Counter | worker_type, result |
Results: success, failure
smg_worker_selection_total¶
Worker selection events by load balancer.
| Type | Labels |
|---|---|
| Counter | worker_type, connection_mode, model, policy |
policy is the routing policy name, for example cache_aware or round_robin.
smg_worker_errors_total¶
Worker-level errors by type, recorded by the HTTP routers when a request to a worker ends in a server error (a 5xx status from the worker or a failed send), and by gRPC prefill-decode dispatch when a leg fails. Regular-mode (non-PD) requests on the gRPC pipeline, which also serves ZMQ workers, are not counted.
| Type | Labels |
|---|---|
| Counter | worker_type, connection_mode, error_type |
Error types: timeout (status 504) and backend_error (other server errors, and every failure on the gRPC PD paths).
smg_kv_event_subscription_failures_total¶
Failures of the per-worker tasks that subscribe to gRPC workers' KV cache events for event-driven cache-aware routing. See Cache-Aware Routing with KV Events.
| Type | Labels |
|---|---|
| Counter | worker, reason |
Reasons: panic, join_error, intern_failed
Overload Protection Metrics¶
A worker whose load report crosses an overload threshold is excluded from routing until the signal recovers; when every candidate is excluded, the request is shed immediately (smg-project/smg#2220). Protection is on when --worker-overload-protection, --worker-overload-token-usage, or --worker-overload-waiting-requests is set, or when a worker spec has an overload block. See Overload Protection.
smg_workers_overloaded¶
Workers currently flagged overloaded and excluded from routing, per model. The flag is evaluated on each load poll, and the gauge changes only when a worker's flag flips.
| Type | Labels |
|---|---|
| Gauge | model |
smg_worker_overload_shed_total¶
Requests shed with 503 worker_overload_protection_shed. The response carries Retry-After set to the load-monitor interval, and the gateway does not retry it internally.
| Type | Labels |
|---|---|
| Counter | stage |
stage |
Cause |
|---|---|
selection |
Every worker the request could use was flagged overloaded |
dispatch |
The selected worker became overloaded between selection and dispatch |
pd_admission |
A gRPC prefill-decode dispatch found no room on the decode engine (see smg_pd_admission_sheds_total); this stage does not depend on overload protection being enabled |
# Overload sheds per second, by stage
sum by (stage) (rate(smg_worker_overload_shed_total[5m]))
# Models with vetoed workers
smg_workers_overloaded > 0Circuit Breaker Metrics¶
smg_worker_cb_state¶
Circuit breaker state per worker.
| Type | Labels | Values |
|---|---|---|
| Gauge | worker |
0 = closed, 1 = open, 2 = half-open, -1 = worker removed |
# Workers with open circuits
count(smg_worker_cb_state == 1)smg_worker_cb_transitions_total¶
Circuit breaker state transitions.
| Type | Labels |
|---|---|
| Counter | worker, from, to |
States: closed, open, half_open
smg_worker_cb_outcomes_total¶
Request outcomes tracked by the circuit breaker.
| Type | Labels |
|---|---|
| Counter | worker, outcome |
Outcomes: success, failure
smg_worker_cb_consecutive_failures¶
Consecutive failures per worker.
| Type | Labels |
|---|---|
| Gauge | worker |
smg_worker_cb_consecutive_successes¶
Consecutive successes per worker.
| Type | Labels |
|---|---|
| Gauge | worker |
Retry Metrics¶
smg_worker_retries_total¶
Retry attempts.
| Type | Labels |
|---|---|
| Counter | worker_type, endpoint |
endpoint uses the same values as smg_router_requests_total. A PD retry counts once for the prefill and once for the decode worker type.
smg_worker_retries_exhausted_total¶
Requests that exhausted all retries.
| Type | Labels |
|---|---|
| Counter | worker_type, endpoint |
smg_worker_retry_backoff_seconds¶
Backoff before each retry, by retry number: attempt="1" is the wait before the first retry, which is the second attempt. No buckets are configured for this metric, so it is exported as a Prometheus summary: quantile series (0, 0.5, 0.9, 0.95, 0.99, 0.999, 1) over a rolling one-minute window, plus _sum and _count.
| Type | Labels |
|---|---|
| Summary | attempt |
# P99 backoff before the second retry (the third attempt)
smg_worker_retry_backoff_seconds{attempt="2", quantile="0.99"}Engine Load Metrics¶
SMG re-exports every worker load report it receives as smg_engine_* gauges (smg-project/smg#2226). By default the load monitor polls every Ready worker every --load-monitor-interval seconds (default 10), so these gauges exist for any worker that reports load. With --disable-load-monitoring, a worker group is polled only when a load-aware routing policy, worker overload protection, or --engine-metrics needs the data; --engine-metrics, a Rust-binary flag the pip smg launch does not accept, forces polling for these gauges alone (see Load Monitoring Configuration). gRPC and ZMQ workers answer the GetLoads load query; HTTP workers are read through their loads endpoint, falling back to parsing vLLM or SGLang /metrics.
Core gauges, one series per DP rank:
| Metric | Type | Labels | Description |
|---|---|---|---|
smg_engine_running_requests |
Gauge | worker, model, dp_rank |
Requests running in the engine |
smg_engine_waiting_requests |
Gauge | worker, model, dp_rank |
Requests waiting in the engine |
smg_engine_token_usage |
Gauge | worker, model, dp_rank |
KV-cache token usage (0.0-1.0) |
smg_engine_gen_throughput |
Gauge | worker, model, dp_rank |
Generation throughput (tokens/s) |
smg_engine_cache_hit_rate |
Gauge | worker, model, dp_rank |
Prefix-cache hit rate (0.0-1.0) |
PD gauges, emitted only for ranks whose report carries a disaggregation section:
| Metric | Type | Labels | Description |
|---|---|---|---|
smg_engine_pd_kv_transfer_latency_ms |
Gauge | worker, role, dp_rank |
KV transfer latency (ms) |
smg_engine_pd_kv_transfer_speed_gb_s |
Gauge | worker, role, dp_rank |
KV transfer speed (GB/s) |
smg_engine_pd_prefill_queue_reqs |
Gauge | worker, role, dp_rank |
Requests in the prefill queue |
smg_engine_pd_decode_queue_reqs |
Gauge | worker, role, dp_rank |
Requests in the decode queue |
role is the engine-reported role: prefill, decode, or null. Values are whatever the engine reports, so which fields are meaningful depends on the engine. When a worker is removed or leaves Ready, its gauges are set to -1. The same load snapshot is available as JSON from GET /loads on the main port.
# KV-cache usage per worker (removed and non-Ready workers report -1)
max by (worker) (smg_engine_token_usage >= 0)
# Requests waiting in engines, per model
sum by (model) (smg_engine_waiting_requests >= 0)Layer 4: Discovery Metrics¶
Metrics for Kubernetes service discovery, emitted when --service-discovery is enabled. The source label is always kubernetes.
smg_discovery_registrations_total¶
Worker registrations submitted by the discovery reconciler.
| Type | Labels |
|---|---|
| Counter | source, result |
Results: success (the registration job was queued), failed
smg_discovery_deregistrations_total¶
Worker removals submitted by the reconciler.
| Type | Labels |
|---|---|
| Counter | source, reason |
Reasons: reconciled
smg_discovery_sync_duration_seconds¶
Duration of a reconcile pass that added or removed workers.
| Type | Labels |
|---|---|
| Histogram | source |
smg_discovery_workers_discovered¶
Workers the reconciler currently wants registered.
| Type | Labels |
|---|---|
| Gauge | source |
Layer 5: MCP Tool Metrics¶
Metrics for Model Context Protocol tool execution.
smg_mcp_tool_calls_total¶
MCP tool invocations.
| Type | Labels |
|---|---|
| Counter | model, tool_name, result |
Results: success, error
# Tool success rate
sum(rate(smg_mcp_tool_calls_total{result="success"}[5m])) / sum(rate(smg_mcp_tool_calls_total[5m]))
# Most used tools
topk(10, sum by (tool_name) (rate(smg_mcp_tool_calls_total[5m])))smg_mcp_tool_duration_seconds¶
Tool execution duration.
| Type | Labels |
|---|---|
| Histogram | model, tool_name |
smg_mcp_servers_active¶
MCP servers the gateway is connected to, updated when a server is registered.
| Type | Labels |
|---|---|
| Gauge | None |
smg_mcp_tool_iterations_total¶
Tool-loop iterations in the Responses API and Anthropic Messages MCP loops.
| Type | Labels |
|---|---|
| Counter | model |
Routing Policy Metrics¶
Decision counters and state gauges of the routing policies. See Load Balancing for the policies themselves.
Cache-Aware Policy Metrics¶
The branch counter and the match-ratio histogram are recorded for each decision made from the approximate prefix trees (--cache-index tree, the default). Decisions made from KV events, in hash mode, or while KV-cache imbalance suspends affinity are not counted. The tree gauges are refreshed after each eviction pass (--eviction-interval, default 120 seconds; the pip smg launch names it --eviction-interval-secs and defaults to 60). See Cache-Aware Routing.
smg_cache_aware_policy_branch_total¶
Outcome of each tree-mode routing decision (smg-project/smg#2515).
| Type | Labels |
|---|---|
| Counter | branch |
branch |
Meaning |
|---|---|
tree_match |
Routed to a worker that holds the matched prefix |
spill |
The match ratio was above --cache-threshold, but the request went to a worker that does not hold the prefix |
expected_wait_fallback |
The match ratio was at or below --cache-threshold, so the worker was chosen by load |
first_healthy_fallback |
Selection returned no worker and the request fell back to the first healthy worker |
smg_cache_aware_match_ratio¶
Best prefix match ratio of each tree-mode decision: matched length divided by input length, 0.0-1.0. Buckets are the deciles 0, 0.1, ..., 1.0; the le="0" bucket counts requests with no cached prefix at all.
| Type | Labels |
|---|---|
| Histogram | None |
smg_cache_tree_chars¶
Characters cached in the string tree (HTTP requests) per model, summed across tenants (smg-project/smg#2134). A prefix held by three workers counts three times.
| Type | Labels |
|---|---|
| Gauge | model |
smg_cache_tree_tokens¶
Tokens cached in the token tree (gRPC requests) per model, summed across tenants.
| Type | Labels |
|---|---|
| Gauge | model |
smg_cache_tree_tenants¶
Tenants (workers) present in each tree.
| Type | Labels |
|---|---|
| Gauge | model, tree |
tree: string, token
smg_cache_placement_entries¶
Placement keys with at least one live holder, per model, in hash mode (--cache-index hash). Refreshed by the sweep that runs every --eviction-interval seconds and drops holders not touched within --cache-ttl-secs.
| Type | Labels |
|---|---|
| Gauge | model |
# Tree-mode decisions by branch
sum by (branch) (rate(smg_cache_aware_policy_branch_total[5m]))
# Median prefix match ratio
histogram_quantile(0.5, sum by (le) (rate(smg_cache_aware_match_ratio_bucket[5m])))
# Token-tree size per model
smg_cache_tree_tokensSticky Session and Manual Policy Metrics¶
The manual policy and sticky sessions (--routing-key-override) share these metrics. See Sticky Sessions.
smg_manual_policy_branch_total¶
Decision branch for keyed routing.
| Type | Labels |
|---|---|
| Counter | branch |
branch |
Meaning |
|---|---|
occupied_hit |
The key is pinned to a healthy worker, which gets the request |
occupied_miss |
The key is pinned, but no pinned worker is healthy; a new worker is chosen |
vacant |
First request for this key; a worker is assigned and pinned |
cap_respill |
Sticky sessions only: the pinned worker is at the per-key in-flight cap, so the request is reassigned |
no_routing_id |
manual policy only: the request carries no routing key |
no_healthy_workers |
No healthy worker is available |
smg_manual_policy_cache_entries¶
Entries in the routing-key map of the manual policy, or of the sticky-session map when --routing-key-override is on.
| Type | Labels |
|---|---|
| Gauge | None |
smg_routing_key_source_total¶
Which source supplied the sticky routing key for a keyed request (smg-project/smg#2206). Recorded when --routing-key-override is on and the policy is not manual or consistent_hashing (those read the key themselves).
| Type | Labels |
|---|---|
| Counter | source |
Sources: rid (the request body's rid, with per-turn and per-retry suffixes stripped), header (the --routing-key-headers, default x-smg-routing-key)
smg_worker_routing_keys_active¶
Distinct routing keys with requests in flight on each worker.
| Type | Labels |
|---|---|
| Gauge | worker |
Hash Policy Metrics¶
smg_consistent_hashing_policy_branch_total¶
Decision branch of the consistent_hashing policy.
| Type | Labels |
|---|---|
| Counter | branch |
branch |
Meaning |
|---|---|
target_worker_hit |
X-SMG-Target-Worker gave the index of a healthy worker |
target_worker_miss |
X-SMG-Target-Worker gave an invalid index or an unhealthy worker; no worker is selected |
routing_key_hit |
Routed by the consistent hash of the routing key |
random_fallback |
No key to hash; a random healthy worker was chosen |
no_healthy_workers |
No workers were available |
smg_prefix_hash_policy_branch_total¶
Decision branch of the prefix_hash policy.
| Type | Labels |
|---|---|
| Counter | branch |
branch |
Meaning |
|---|---|
ring_hit |
The hash-ring owner of the prefix had acceptable load |
load_balance_walk |
The ring owners were overloaded, so the request went to the least-loaded acceptable worker |
fallback_least_load |
No hash ring was available; the least-loaded worker was chosen |
no_routing_key |
Nothing to hash; the least-loaded worker was chosen |
no_healthy_workers |
No healthy worker was available |
Priority Scheduler Metrics¶
With --priority-scheduler-enabled (a Rust-binary flag the pip smg launch does not accept), the priority scheduler replaces the concurrency limiter and exports its own metrics: smg_scheduler_admit_total, smg_scheduler_queue_wait_seconds, smg_scheduler_preemption_total, smg_scheduler_clamp_total, smg_scheduler_unknown_priority_value_total, smg_scheduler_starvation_promotion_total, smg_scheduler_inflight, smg_scheduler_queue_depth, smg_scheduler_queue_size_limit, smg_scheduler_utilization, and smg_scheduler_class_capacity_pressure. Their types, labels, and values are listed in the Priority Scheduler Reference.
smg_scheduler_queue_wait_seconds has no configured buckets, so like smg_worker_retry_backoff_seconds it is exported as a summary (quantile series plus _sum and _count).
RL Control Plane Metrics¶
Emitted only when the RL control plane is mounted with --enable-rl. See RL Control Plane.
| Metric | Type | Labels | Description |
|---|---|---|---|
smg_rl_control_calls_total |
Counter | op, result |
Control calls proxied to engines |
smg_rl_control_call_duration_seconds |
Histogram | op |
Latency of one proxied control call |
smg_rl_fanout_total |
Counter | result |
Fan-out requests |
smg_rl_fanout_duration_seconds |
Histogram | None | Wall time of one fan-out request |
op: one ofpause_generation,continue_generation,update_weights_from_disk,update_weights_from_tensor,update_weights_from_distributed,init_weights_update_group,destroy_weights_update_group,update_weight_version,flush_cache,abort_request,release_memory_occupation,resume_memory_occupation,pause,resume,sleep,wake_up,collective_rpc,server_info,get_server_info,health; any other path isotherresultfor control calls:ok(2xx from the engine),upstream_error(non-2xx),timeout(no answer within--rl-control-timeout-secs),unreachableresultfor fan-outs:ok(every target succeeded),partial(at least one target failed),no_match(the selector matched no worker)
# Failed control calls by operation
sum by (op, result) (rate(smg_rl_control_calls_total{result!="ok"}[5m]))HA Mesh Metrics¶
Emitted when the HA mesh is enabled (--enable-mesh). These names use the router_ prefix rather than smg_. See High Availability.
| Metric | Type | Labels | Description |
|---|---|---|---|
router_mesh_peer_connections |
Gauge | peer |
Recorded by the node that accepts a sync stream: 1 while the stream from the peer is open, 0 after it ends |
router_mesh_peer_reconnects_total |
Counter | peer |
Accepted sync streams from the peer that ended |
router_mesh_sync_round_duration_seconds |
Histogram | peer |
Duration of one outbound sync round to the peer, recorded by the dialing node |
When a stream opens, router_mesh_peer_connections is first set to 1 under an empty peer label, before the peer identifies itself; once the peer has identified itself, that empty-peer series stays at 1. Exclude it with peer!="":
# Active peer links
count(router_mesh_peer_connections{peer!=""} == 1)Declared but Not Emitted¶
The v1.11.0 code registers descriptions for the following names, but nothing records values for them, so they never appear on /metrics. Remove them from dashboards and alerts:
smg_router_stage_duration_secondssmg_worker_connections_activesmg_db_operations_total,smg_db_operation_duration_seconds,smg_db_connections_active,smg_db_items_storedrouter_mesh_store_cardinality,router_mesh_store_hash,router_rl_drift_ratio,router_lb_drift_ratio
router_mesh_peer_ack_total and router_mesh_peer_nack_total have recording code, but they count legacy ACK and NACK stream messages, which v1.11.0 nodes never send, so they stay absent in a v1.11.0 mesh.
Dashboard Queries Summary¶
| Metric | Query |
|---|---|
| Request rate | sum(rate(smg_http_requests_total[5m])) |
| Error rate | sum(rate(smg_http_responses_total{status_code=~"5.."}[5m])) / sum(rate(smg_http_responses_total[5m])) |
| P99 latency | histogram_quantile(0.99, sum by (le) (rate(smg_http_request_duration_seconds_bucket[5m]))) |
| TTFT P50 | histogram_quantile(0.5, sum by (le) (rate(smg_router_ttft_seconds_bucket[5m]))) |
| Tokens/sec | sum(rate(smg_router_tokens_total[5m])) |
| Healthy workers | count(smg_worker_health == 1) |
| Open circuits | count(smg_worker_cb_state == 1) |
| Rate limit rejections | sum(rate(smg_http_rate_limit_total{result="rejected"}[5m])) |
| Admission queue depth | smg_admission_queue_depth |
| Overload sheds | sum by (stage) (rate(smg_worker_overload_shed_total[5m])) |
| PD admission sheds | rate(smg_pd_admission_sheds_total[5m]) |
| Tokenizer cache hit ratio | sum by (layer) (rate(smg_tokenizer_cache_lookups_total{result="hit"}[5m])) / sum by (layer) (rate(smg_tokenizer_cache_lookups_total[5m])) |
| Allocator resident memory | smg_allocator_resident_bytes |
| Event-loop stalls | rate(smg_tokio_event_loop_stalls_total[5m]) |
| MCP tool success rate | sum(rate(smg_mcp_tool_calls_total{result="success"}[5m])) / sum(rate(smg_mcp_tool_calls_total[5m])) |
Histogram Buckets¶
Default buckets (29 buckets from 1 ms to 7200 s) apply to every histogram whose name ends in duration_seconds, ttft_seconds, or tpot_seconds:
0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0,
10.0, 15.0, 30.0, 45.0, 60.0, 90.0, 120.0, 180.0, 240.0, 300.0,
480.0, 900.0, 1200.0, 1800.0, 2700.0, 3600.0, 5400.0, 7200.0Configure custom buckets for these histograms via CLI:
smg launch --prometheus-duration-buckets 0.01 0.1 0.5 1 5 10Two histograms have fixed buckets of their own: smg_tokio_event_loop_delay_seconds (0 to 1 s) and smg_cache_aware_match_ratio (deciles). A histogram that matches none of these rules is exported as a Prometheus summary instead; in v1.11.0 that applies to smg_worker_retry_backoff_seconds and smg_scheduler_queue_wait_seconds.