Prometheus Metrics
SwarmLLM exposes a Prometheus-compatible metrics endpoint at GET /metrics. It answers without a key only to a request made on the node's own machine, and not even then when api.metrics_auth_required = true; from anywhere else send Authorization: Bearer <api key> (see Monitoring).
Available Metrics
Core Metrics
| Metric | Type | Description |
|---|---|---|
swarmllm_peers_connected | gauge | Number of connected peers |
swarmllm_inference_requests_total | counter | Total inference requests processed |
swarmllm_credits_balance | gauge | The internal credit ledger's figure — dormant, it gates nothing and is self-minted |
swarmllm_shards_hosted | gauge | Number of locally hosted shards |
swarmllm_network_bytes_total{direction="in"|"out"} | counter | Bytes sent and received over the peer network. Counted at the transport, so it covers every protocol — gossip, DHT maintenance, shard transfers, inference — not only what this node's own code writes. Omitted entirely when nothing is counting rather than reported as zero: a flat line at 0 would be read as "no traffic" when it means "not measured" |
swarmllm_inference_latency_seconds | histogram | Inference request latency |
swarmllm_inference_requests_by_route_total{route,outcome} | counter | Completed requests by route and outcome |
swarmllm_empty_replies_total | counter | Replies the model generated that finalisation removed entirely (control tokens, or a stop sequence matching at once) — delivered to the client as an empty success |
swarmllm_shard_bytes_total{direction="in"|"out"} | counter | Bytes of model parts served to peers and fetched from them — the only traffic resources.max_bandwidth_mbps throttles |
swarmllm_gossip_bytes_total{topic,direction} | counter | Gossip bytes by topic and direction (message length only; framing and encryption are in swarmllm_network_bytes_total) |
swarmllm_credits_earned_total, swarmllm_credits_reserved_total, swarmllm_credits_returned_total | counter | The dormant credit ledger's lifetime figures — they gate nothing |
route is one of local, split, distributed, relayed, cloud; outcome
is ok, error, cancelled or pending. Both are closed sets, so this metric
is 20 series regardless of how large the swarm grows. Per-peer, per-model and
per-shard breakdowns are deliberately not exported here — that label set grows
with the swarm and would eventually break the scrape. Fetch them from
GET /api/admin/performance instead, which is served on request and retains
nothing.
OpenTelemetry GenAI Metrics
Named to the OpenTelemetry GenAI semantic conventions so an OTel collector and the community Grafana dashboards work without a translation layer.
| Metric | Type | Description |
|---|---|---|
gen_ai_server_time_to_first_token_seconds | histogram | Queue + prefill: how long until the first token |
gen_ai_server_time_per_output_token_seconds | histogram | Decode cost per token after the first |
These are the two figures that separate a backed-up queue from slow generation;
end-to-end latency alone cannot. swarmllm_inference_latency_seconds is the same
measurement as the conventions' gen_ai.server.request.duration under a local
name, and both are exported while dashboards migrate.
Only requests that emitted an incremental token contribute to these histograms. A non-streaming path never stamps a first token, so there is no honest way to split decode out of its total and it is omitted rather than counted as zero.
Serving-Side Metrics
Work this node performed for other peers. Every metric above measures requests this node made; these measure what it gave.
| Metric | Type | Description |
|---|---|---|
swarmllm_segments_served_total | counter | Pipeline segments computed for other peers |
swarmllm_layers_served_total | counter | Transformer layers computed for other peers |
swarmllm_segment_serve_seconds_total | counter | Cumulative compute time spent serving |
swarmllm_segment_activation_bytes_total | counter | Activation bytes returned to peers |
rate(swarmllm_segment_serve_seconds_total[5m]) / rate(swarmllm_layers_served_total[5m])
gives seconds per layer served — the figure other peers' schedulers actually rank
this node on.
Channel Metrics
Internal channel health metrics for monitoring backpressure:
| Metric | Type | Description |
|---|---|---|
swarmllm_channel_capacity{channel="..."} | gauge | Channel buffer capacity |
swarmllm_channel_sent_total{channel="..."} | counter | Messages sent through channel |
swarmllm_channel_dropped_total{channel="..."} | counter | Messages dropped due to backpressure |
Emitted for the channels whose send paths actually count: network_out,
rebalance and pool_cmd. A channel absent from the output is
one nothing increments, which is deliberate — a perpetual zero cannot be told
apart from a healthy channel, and an alert on it can never fire.
swarmllm_channel_dropped_total rising is worth an alert, but the log is where
the severity is: a drop line carries nothing_accepted_for_secs, and that is
what separates a momentary burst from a consumer that has stopped. Anything
above a few seconds on network_out means this node is not receiving from the
swarm at all (docs/FUTURE_WORK.md #90). The line is rate-limited to one per
30 s per channel and reports how many it swallowed.
Histogram Buckets
swarmllm_inference_latency_seconds uses these bucket boundaries (in seconds):
0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, +Inf
The two gen_ai_server_* histograms use the boundaries published in the
OpenTelemetry GenAI conventions rather than locally chosen ones, so buckets line
up with every other GenAI server a collector scrapes:
0.001, 0.005, 0.01, 0.02, 0.04, 0.06, 0.08, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0, +Inf
For every histogram, _count and _sum come from monotonic counters rather than
the in-memory sample ring. The ring is both size- and age-bounded, so its length
falls when it wraps, which would break rate() and increase().
Scraping Configuration
Add to your prometheus.yml:
scrape_configs:
- job_name: "swarmllm"
static_configs:
- targets: ["localhost:8800"]
Example Queries
# Request rate (requests per second over 5 minutes)
rate(swarmllm_inference_requests_total[5m])
# P50 latency
histogram_quantile(0.50, rate(swarmllm_inference_latency_seconds_bucket[5m]))
# P99 latency
histogram_quantile(0.99, rate(swarmllm_inference_latency_seconds_bucket[5m]))
# Average latency
rate(swarmllm_inference_latency_seconds_sum[5m]) / rate(swarmllm_inference_latency_seconds_count[5m])
# P95 time to first token — is the queue backed up, or is generation slow?
histogram_quantile(0.95, rate(gen_ai_server_time_to_first_token_seconds_bucket[5m]))
# P95 per-token decode cost
histogram_quantile(0.95, rate(gen_ai_server_time_per_output_token_seconds_bucket[5m]))
# Share of requests that left this machine
sum(rate(swarmllm_inference_requests_by_route_total{route=~"distributed|relayed"}[5m]))
/ sum(rate(swarmllm_inference_requests_by_route_total[5m]))
# Error rate by route
sum by (route) (rate(swarmllm_inference_requests_by_route_total{outcome="error"}[5m]))
# Seconds of compute contributed per layer served
rate(swarmllm_segment_serve_seconds_total[5m]) / rate(swarmllm_layers_served_total[5m])
Health Check
GET /health/ready
Readiness probe returning subsystem status. Returns 200 when ready, 503 otherwise. No auth required.
{
"ready": true,
"subsystems": {
"network": true,
"inference_router": true,
"api_server": true,
...
}
}