Monitoring and Metrics¶
Illustrative mutation fragments
The management mutations on this page are non-standalone fragments. Before adapting them, follow this site's Example Quality Contract and complete Quickstart lifecycle. Do not treat HTTP success or management readback alone as runtime proof.
LoxiLB exposes Prometheus metrics for load balancing, system pressure, AI admission, token quotas, engine-specific routing, and fullproxy QoS. Use the metrics to distinguish Gateway pressure from host pressure and policy denials from backend failures.
For the complete generated list of 197 release-scope families, exact labels, activation classes, and evidence status, see the Metrics Reference. The catalog records source evidence; a raw scrape and controlled stimulus are still required for a deployment claim.
Enable and scrape metrics¶
Metrics collection is disabled by default. Enable it at process startup with -p or
--prometheus, or dynamically through the authenticated control plane. While disabled,
/metrics returns 503.
export CONTROL_API="https://gateway.example.com/netlox/v1"
install -m 600 /dev/null ./control-plane.headers
printf 'Authorization: Bearer %s\n' "$CONTROL_PLANE_TOKEN" > ./control-plane.headers
curl --fail-with-body --silent --show-error \
--request POST \
--header @control-plane.headers \
"$CONTROL_API/config/metrics"
curl --fail-with-body --silent --show-error \
--header @control-plane.headers \
"$CONTROL_API/config/metrics" | jq .
The Prometheus scrape route is GET /netlox/v1/metrics. Its current API
contract explicitly exempts it from bearer authentication so Prometheus can
scrape without a short-lived user token.
Unauthenticated does not mean public
Metrics expose service, model, tenant, endpoint, and policy activity. Restrict the listener with network policy, firewall rules, or a trusted monitoring proxy. Use TLS across untrusted networks and keep tenant/model labels free of secrets or personal information.
scrape_configs:
- job_name: loxilb-inference-gateway
metrics_path: /netlox/v1/metrics
scrape_interval: 10s
scheme: https
static_configs:
- targets: ["gateway.example.com"]
The bundled monitoring stack and dashboards are available under the public
repository's deploy/monitoring/ directory. Review image versions, network
bindings, credentials, and TLS settings before using it outside a lab.
CPU scope: Gateway container versus host¶
flowchart LR
P["Gateway metrics sampler"] --> C{"Container detected and<br/>cgroup CPU readable?"}
C -->|yes| S["system CPU gauge<br/>whole cgroup / CPU allowance"]
C -->|no| F["system CPU gauge<br/>host /proc/stat fallback"]
P --> H["host CPU gauge<br/>always host /proc/stat"]
style S fill:#e8f5e9,stroke:#43a047
style F fill:#fff9c4,stroke:#f9a825
style H fill:#e1f5fe,stroke:#0288d1
| Metric | Container behavior | Bare-metal behavior |
|---|---|---|
loxilb_system_cpu_utilization_percent |
Whole Gateway cgroup/container CPU usage as a percentage of its allowed CPU cores | Whole host CPU usage |
loxilb_host_cpu_utilization_percent |
Whole host CPU usage | Whole host CPU usage |
The system gauge is not necessarily the LoxiLB process alone: helper processes in the same cgroup contribute. An explicit container CPU quota defines the allowance; otherwise available/affinity-constrained cores are used. The value is clamped to 0–100 percent.
If a container is detected but cgroup v1/v2 accounting is unreadable, the
system gauge falls back to host /proc/stat. On bare metal the two gauges are
therefore expected to be equal.
Use both gauges together:
- Scoped high, host moderate: Gateway container/cgroup is near its allowance.
- Scoped low, host high: another host workload may be causing contention.
- Equal values: bare metal or cgroup fallback; confirm the startup log's CPU accounting source before attributing pressure.
AI access and request metrics¶
| Metric | Type | Labels | Meaning |
|---|---|---|---|
loxilb_ai_requests_total |
Counter | model, tenant, status, outcome |
Completed SSE inference streams use outcome="completed"; admission denials use outcome="denied". Non-SSE successes are not counted here. |
loxilb_ai_request_duration_seconds |
Histogram | model, tenant |
Duration recorded when an SSE inference stream completes; non-SSE requests are not observed |
loxilb_ai_active_streams |
Gauge | model |
Active SSE streams |
loxilb_ai_rate_limit_hits_total |
Counter | tenant, reason |
RPS and token-quota denials |
loxilb_ai_model_not_allowed_total |
Counter | model, tenant |
Model authorization denials |
Denials are recorded at the decision point. Do not assume a generic completed
request query includes every pre-dispatch denial; use the dedicated denial
counters when alerting on 403 and 429.
JWT and policy-store metrics¶
| Metric | Type | Labels | Meaning |
|---|---|---|---|
loxilb_ai_jwt_validation_total |
Counter | tenant, reason |
Bearer verdicts; allowed is admission, while denial reasons use the closed client error-code set. Tenant - means no verified tenant was available. |
loxilb_ai_jwks_refresh_total |
Counter | profile, outcome |
JWKS fetch attempts; outcome is success or failure |
loxilb_ai_jwks_keys |
Gauge | profile |
Usable verification keys in the current snapshot |
loxilb_ai_jwks_usable |
Gauge | profile |
1 only when a fetched keyset is inside the staleness cutoff |
loxilb_ai_jwks_last_success_timestamp_seconds |
Gauge | profile |
Last successful fetch time; absent before the first success |
loxilb_ai_policy_store_unavailable_total |
Counter | none | Requests refused 503 because a required credential or quota policy could not be evaluated |
A rising JWKS failure counter with a flat success counter is a warning while
the last-known-good keyset still admits traffic. Alert before
loxilb_ai_jwks_usable becomes 0. Do not treat an absent last-success series
as a zero timestamp; it means no fetch has succeeded.
# JWT decisions by tenant and closed reason code
sum by (tenant, reason) (
rate(loxilb_ai_jwt_validation_total[5m])
)
# JWKS refresh failures by profile
sum by (profile) (
rate(loxilb_ai_jwks_refresh_total{outcome="failure"}[5m])
)
# Profiles whose currently cached keyset is unusable
loxilb_ai_jwks_usable == 0
Token quota metrics¶
| Metric | Type | Meaning |
|---|---|---|
loxilb_ai_tokens_consumed_total{model,tenant,kind} |
Counter | Tokens charged; kind is prompt or completion |
loxilb_ai_tokens_estimated_total{model,tenant} |
Counter | Tokens charged from the estimate path |
loxilb_ai_tokens_missing_total{model,tenant} |
Counter | Completed responses without readable usage |
loxilb_ai_token_quota_denied_total{tenant} |
Counter | Requests denied by token quota |
loxilb_ai_token_quota_utilization{tenant} |
Gauge | Aggregate spent fraction after continuous refill |
loxilb_ai_token_quota_limit_tokens{tenant} |
Gauge | Aggregate TPM limit |
loxilb_ai_token_quota_model_utilization{tenant,model} |
Gauge | Model-specific spent fraction |
loxilb_ai_token_quota_model_limit_tokens{tenant,model} |
Gauge | Model-specific TPM limit |
loxilb_ai_user_token_quota_utilization{tenant,user} / _limit_tokens |
Gauge | User TPM state and last charged limit |
loxilb_ai_user_model_token_quota_utilization{tenant,user,model} / _limit_tokens |
Gauge | User-and-model TPM state |
loxilb_ai_key_token_quota_utilization{key_id} / _limit_tokens |
Gauge | Implemented per-key TPM state; primary Swagger text is stale and release support remains pending convergence |
loxilb_ai_vip_token_quota_utilization{service} / _limit_tokens |
Gauge | Shared service TPM state |
loxilb_ai_token_quota_cold_open_total |
Counter | Quota service began with empty state because no peers were available, peer warmup was disabled, or peer warmup timed out |
Utilization can exceed 1 while a completed response has created post-hoc
debt. It decays as the smooth bucket refills. Aggregate and model gauges are
separate admission gates and must not be added together.
Useful queries:
# Tokens charged per second, split by count source kind
sum by (kind) (rate(loxilb_ai_tokens_consumed_total[5m]))
# Estimated-accounting share; clamp avoids division by zero
sum(rate(loxilb_ai_tokens_estimated_total[5m]))
/
clamp_min(sum(rate(loxilb_ai_tokens_consumed_total[5m])), 1)
# Remaining aggregate headroom in tokens
loxilb_ai_token_quota_limit_tokens
* (1 - loxilb_ai_token_quota_utilization)
Fullproxy QoS metrics¶
The loxilb_proxy_qos_* family exists only for an actively shaped fullproxy
service. Every series uses vip, port, proto, and direction labels;
direction is upload or download.
| Metric | Type | Unit |
|---|---|---|
loxilb_proxy_qos_bytes_passed_total |
Counter | Plaintext payload bytes |
loxilb_proxy_qos_bytes_delayed_total |
Counter | Payload bytes that waited |
loxilb_proxy_qos_parks_total |
Counter | Pause events |
loxilb_proxy_qos_park_seconds_total |
Counter | Seconds for parks that resumed |
loxilb_proxy_qos_parked_connections |
Gauge | Connections currently paused |
loxilb_proxy_qos_tokens_bytes |
Gauge | Bucket level in bytes |
loxilb_proxy_qos_cir_bytes_per_second |
Gauge | Committed rate in bytes/s |
loxilb_proxy_qos_cbs_bytes |
Gauge | Burst depth in bytes |
The policy API configures Mbps, while the shaper CIR metric exports bytes/s. For example, 16 Mbps is 2,000,000 bytes/s. Do not multiply the metric by eight unless the panel is deliberately converting it to bits/s and labels the result accordingly.
# Payload throughput in bytes/s
sum by (vip, port, direction) (
rate(loxilb_proxy_qos_bytes_passed_total[5m])
)
# Fraction of passed payload that encountered shaping
sum by (vip, port, direction) (
rate(loxilb_proxy_qos_bytes_delayed_total[5m])
)
/
clamp_min(
sum by (vip, port, direction) (
rate(loxilb_proxy_qos_bytes_passed_total[5m])
), 1
)
Detached services disappear from this metric family after the collection refresh. A frozen series after detach is not the expected representation.
Engine and P/D metrics¶
| Metric | Meaning |
|---|---|
loxilb_ai_engine_info{service,engine} |
Engine identity currently emitted only for llama.cpp rules |
loxilb_ai_llamacpp_probe_warnings_total{service,kind} |
llama.cpp /props findings such as model/build/slot mismatch, sleeping, or unanswered |
loxilb_ai_pd_requests_total{model,phase,status} |
One terminal P/D outcome: complete/success; prefill timeout/error/rejected; decode timeout/error; or defensive unknown/error |
loxilb_ai_pd_prefill_duration_seconds{model} |
Observed prefill phase duration when known |
loxilb_ai_pd_decode_ttft_seconds{model} |
Decode time to first token/byte when known |
loxilb_ai_pd_kv_params_found_total{model} |
A completed prefill response carried kv_transfer_params |
loxilb_ai_pd_kv_params_missing_total{model} |
A prefill response was actually inspected but lacked kv_transfer_params; prefill failures do not increment it |
loxilb_ai_pd_session_hits_total{model} |
Tier-0 P/D session-stickiness selections |
loxilb_ai_pd_tier_selected_total{tier,model} |
Successful terminal prefill selections by tier0, tier1, tier15, or tier2; pre-routing admission outcomes increment nothing |
loxilb_pd_admission_shed_total |
Every eligible prefill endpoint was at its in-flight cap and queueing was disabled; request refused |
loxilb_pd_admission_queued_total |
Request parked in a per-endpoint FIFO because the pool was capped and queueing was enabled |
loxilb_pd_admission_overflow_shed_total |
Pool and eligible per-endpoint FIFOs were full; overflow request refused |
loxilb_pd_sg_prefill_abort_decode_total |
SGLang prefill failure aborted the decode leg |
loxilb_pd_sg_decode_close_drain_total |
SGLang decode failure closed the prefill drain leg |
loxilb_pd_sg_room_retry_total |
SGLang pair retried with a new bootstrap room |
loxilb_pd_sg_prefill_reject_relay_total |
SGLang prefill 4xx relayed and decode aborted |
loxilb_pd_sg_oversize_reject_total |
SGLang request rejected because bootstrap injection was impossible |
loxilb_pd_trt_ctx_early_exit_total |
TensorRT-LLM context stage completed the request without generation stage |
loxilb_pd_cb_flips_total |
All P/D circuit-breaker state transitions; correlate a rate increase with endpoint health and status |
loxilb_pd_cb_proactive_heal_total |
Open-to-half-open transitions initiated by the periodic health pass |
loxilb_pd_connect_failover_total |
Prefill TCP-connect failures successfully retried on another healthy prefill endpoint |
loxilb_pd_prefill_ep_died_total |
Prefill connections that died mid-request and returned 503 |
loxilb_pd_decode_ep_died_total |
Decode connect failures or EOF before response relay |
An engine info series proves the rule identity was registered; it does not
prove backend correctness or traffic success. A successful connect-failover
counter also does not prove that origin HTTP 5xx responses were hidden:
correlate circuit transitions and endpoint-death counters with
loxilb_proxy_http_responses_by_status_total, endpoint health, logs, and the
client-visible result.
For loxilb_ai_pd_requests_total, alert on the typed phase and status pair rather than an
HTTP-code guess. Reconcile tier_selected only against successfully selected P/D requests;
parked and refused admission events belong to the three admission families and intentionally do
not choose a routing tier.
Worker scrape metrics¶
loxilb_ai_worker_scrape_total{result} records the outcome of worker metric
collection. The closed result set is ok, unreachable, http_error,
body_error, unparseable, bad_request, and unknown. Treat each class as
a distinct failure stage rather than collapsing every non-ok result into
backend unavailability.
# Worker scrape attempts by result
sum by (result) (
rate(loxilb_ai_worker_scrape_total[5m])
)
# Fraction of worker scrapes that did not complete successfully
sum(rate(loxilb_ai_worker_scrape_total{result!="ok"}[5m]))
/
clamp_min(sum(rate(loxilb_ai_worker_scrape_total[5m])), 1)
These counters prove the collection path classified an attempt; they do not prove that a worker's reported values were fresh or that inference traffic reached that worker. Correlate them with worker-series freshness, endpoint health, and an independent backend receipt.
Relay cache and backpressure metrics¶
The fullproxy relay cache is bounded per connection but not by one aggregate process-wide cap. Slow backends and many concurrent request bodies can therefore raise total cached bytes even when no single connection reaches its watermark.
| Metric | Meaning |
|---|---|
loxilb_proxy_cache_bytes |
Relay payload bytes currently cached across all connections |
loxilb_proxy_cache_bytes_max_conn |
Largest cache held by one connection |
loxilb_proxy_cache_conns_queued |
Connections currently holding cached relay payload |
loxilb_proxy_cache_backpressure_ratio |
Fraction of connections with active cache backpressure |
loxilb_proxy_cache_high_water_events_total |
Per-connection high-water activations |
loxilb_proxy_cache_drain_partial_total |
Partial drains caused by socket flow control |
loxilb_proxy_graceful_close_total |
Graceful closes that drained pending cached data |
The normal per-connection watermark is 12 MiB and the chunked watermark is
768 KiB. loxilb_proxy_cache_bytes_max_conn approaching the applicable value
means one connection is near backpressure; rising aggregate bytes with a low
maximum points to concurrency rather than a single oversized body.
# Mean cached payload per queued connection
loxilb_proxy_cache_bytes
/
clamp_min(loxilb_proxy_cache_conns_queued, 1)
# New high-water activations
rate(loxilb_proxy_cache_high_water_events_total[5m])
Configuration recovery metrics¶
| Metric | Meaning |
|---|---|
loxilb_snapshot_total{trigger} |
Captures produced by manual, write-through, and pre-restore; scheduled and pre-upgrade series are precreated/reserved and remain zero because no current producer invokes those triggers |
loxilb_restore_total{mode,result} |
Dry-run, commit, and boot restore outcomes |
loxilb_restore_duration_seconds |
Restore pipeline duration |
loxilb_last_restore_timestamp_seconds |
Last successful committed or boot restore |
loxilb_boot_config_conflict_total |
Boot arbitration between snapshot and legacy files |
Page immediately on a new
loxilb_restore_total{result="ROLLBACK-FAILED"}. Investigate repeated conflict
increments and rejected/error restore results. See
Configuration Backup and Restore for the procedure.
Peer synchronization metrics¶
The xSync metric family exposes loss and receiver-side rejection on the sockproxy peer path:
| Metric | Meaning |
|---|---|
loxilb_sockproxy_sync_overflow_total{kind} |
Inbound event or per-peer outbound queue overflow; events/batches use drop-oldest behavior |
loxilb_sockproxy_sync_drop_total{reason} |
Batch dropped after retry exhaustion |
loxilb_sockproxy_sync_apply_errors_total |
Receiver could not apply a synchronized entry |
loxilb_sockproxy_sync_health_reject_total{reason} |
Receiver rejected an entry through its local endpoint-health gate |
loxilb_sockproxy_sync_conflict_total{outcome} |
Active-active conflict-resolution result |
loxilb_sockproxy_sync_push_latency_seconds{peer,rpc} |
Sender-side RPC latency |
loxilb_sockproxy_sync_inflight_rpc{peer} |
Currently in-flight synchronization RPCs |
loxilb_sockproxy_sync_peer_scope_version{peer} |
Quota-state wire scope version reported for the peer; equality is required before treating quota synchronization as compatible |
Page on new overflow, retry-exhausted drops, or apply errors during normal load. A quiet metric does not prove that a peer is connected: correlate with peer state, network evidence, and a controlled state change. Current xSync ports do not authenticate or encrypt peers themselves; see HA and Upgrade Limitations.
Sockmap observability boundary¶
The release-scope manifest contains no dedicated sockmap Prometheus family, so
there is no valid sockmap-specific PromQL to document. Verify acceleration with
the sockmap_stats BPF map, active-map state, the reset operation, byte
equivalence, and an off control as described in
Sockmap Acceleration. Do not substitute the
loxilb_sockproxy_sync_* peer-synchronization family: it describes xSync, not
sockmap accelerator engagement.
Response-only sockmap acceleration may be combined with api_key_auth on
current Gateway main because requests still traverse userspace admission.
Those accelerated responses are not recorded by the userspace response path,
so a flat AI response counter is not proof of failure. Request and
bidirectional acceleration remain incompatible with any api_key_auth
declaration.
OPA and optional DPU metrics¶
OPA watcher signals are
loxilb_opa_watcher_syncs_total{status},
loxilb_opa_sync_duration_seconds, loxilb_opa_firewall_rules, and
loxilb_opa_circuit_breaker_state (0 closed, 1 open, 2 half-open). The
watcher is currently an experimental security integration; read its
production boundary before using these signals.
DPU/DOCA metrics are registered only after a DPU plugin attaches. Core signals
include doca_offload_active_flows, doca_offload_attempts_total,
doca_offload_failures_total, doca_circuit_breaker_state, and
doca_pipe_hw_bytes_total{pipe,direction}. Absence on a standard non-DPU image
is expected. See DPU Offload Observability.
Core health metrics¶
| Metric | Type | Meaning |
|---|---|---|
loxilb_healthy_endpoints |
Gauge | Endpoints passing configured health checks |
loxilb_unhealthy_endpoints |
Gauge | Endpoints failing health checks |
loxilb_lb_rules |
Gauge | Configured LB rules |
loxilb_active_conntrack_entries |
Gauge | Sampled active connection-tracking entries |
loxilb_proxy_http_ttfb_seconds |
Histogram | Fullproxy time to first byte |
Some metric families are created only after the corresponding feature or traffic path is used. Send one safe warm-up request before declaring a series missing.
Verify end to end¶
curl --fail-with-body --silent --show-error "$CONTROL_API/metrics" \
| grep -E '^loxilb_(system_cpu|host_cpu|healthy_endpoints|lb_rules)'
Then verify Prometheus:
The result should be 1. Open the provisioned dashboards and compare one
panel with the raw series before relying on its units or aggregation.
Troubleshooting¶
| Symptom | Likely cause | Action |
|---|---|---|
/metrics returns 503 |
Metrics are disabled | Enable through /config/metrics and retry |
/metrics returns 401 |
A proxy or deployment policy is adding auth | Check the actual request path; current Gateway route itself is auth-exempt |
| Expected series is absent | Feature unused or lazy registration | Generate one safe request and confirm feature configuration |
| System CPU looks like host CPU in a container | Cgroup accounting unreadable | Check CPU-source startup log and cgroup mounts/permissions |
| Host CPU high, scoped CPU low | Unrelated host workload | Inspect host processes and scheduling pressure before restarting Gateway |
| Quota usage seems doubled | Aggregate and model gauges were summed | Display them as separate gates |
JWT requests switch from success to 503 |
JWKS never fetched or last-known-good set became stale | Compare usable, keys, last-success, and refresh outcomes for the profile |
401 invalid_api_key rises with no store alert |
Unknown/disabled/expired client key | Investigate credential lifecycle; do not classify as a store outage |
policy_store_unavailable_total rises |
Required policy cannot be evaluated | Restore the key/JWKS/quota dependency and verify backend receipt delta remains 0 |
| QoS rate is eight times smaller than API number | Bits-versus-bytes mismatch | API is Mbps; shaper metric is bytes/s |
| llama.cpp warning rate increases | Fleet consistency or sleeping endpoint | Inspect kind and compare endpoint /props safely |
| Aggregate relay cache rises while max-connection cache stays low | Many concurrent slow drains | Correlate queued connections, backpressure ratio, backend latency, and memory |
Restore result is ROLLBACK-FAILED |
Commit and automatic rollback both failed | Isolate the node and follow the recovery procedure |
| xSync overflow or drop counter increases | Slow/unreachable peer or event pressure | Restrict promotion, inspect peer reachability and push latency, and verify state before serving |
| DPU metrics are absent | No plugin attached or non-DPU build | Verify the immutable build and plugin state before treating it as a scrape fault |
Security and cleanup¶
Use low-cardinality, non-secret labels. Restrict Prometheus and Grafana access, change default credentials, validate TLS, and apply retention appropriate for tenant activity metadata. Remove temporary header files after management calls: