Prometheus Metrics
Exposes extension metrics in Prometheus text exposition format. No external dependencies. Zero overhead when not enabled - every metric call uses optional chaining on a nullish reference, so V8 short-circuits on a single pointer check.
Setup
// src/lib/server/metrics.js
import { createMetrics } from 'svelte-adapter-uws-extensions/prometheus';
export const metrics = createMetrics({
prefix: 'myapp_',
mapTopic: (topic) => topic.startsWith('room:') ? 'room:*' : topic
});
Pass the metrics object to any extension via its options:
import { metrics } from './metrics.js';
import { redis } from './redis.js';
import { createPresence } from 'svelte-adapter-uws-extensions/redis/presence';
import { createPubSubBus } from 'svelte-adapter-uws-extensions/redis/pubsub';
import { createReplay } from 'svelte-adapter-uws-extensions/redis/replay';
import { createRateLimit } from 'svelte-adapter-uws-extensions/redis/ratelimit';
import { createGroup } from 'svelte-adapter-uws-extensions/redis/groups';
import { createCursor } from 'svelte-adapter-uws-extensions/redis/cursor';
export const bus = createPubSubBus(redis, { metrics });
export const presence = createPresence(redis, { metrics, key: 'id' });
export const replay = createReplay(redis, { metrics });
export const limiter = createRateLimit(redis, { points: 10, interval: 1000, metrics });
export const lobby = createGroup(redis, 'lobby', { metrics });
export const cursors = createCursor(redis, { metrics });
Mounting the endpoint
With uWebSockets.js:
app.get('/metrics', metrics.handler);
Or use metrics.serialize() to get the raw text and serve it however you like.
The recommended deployment shape is to bind /metrics behind a network barrier - a private port, an internal load balancer, or a sidecar scrape target - so the operating-system perimeter does the access control. When that is not available (same-listener mounts, scrape-from-public-network setups), use metrics.authedHandler(predicate) for application-level auth:
import { timingSafeEqual } from 'node:crypto';
const expected = Buffer.from(process.env.SCRAPE_TOKEN);
app.get('/metrics', metrics.authedHandler((res, req) => {
const provided = req.getHeader('x-scrape-token');
if (!provided) return false;
const got = Buffer.from(provided);
if (got.length !== expected.length) return false;
return timingSafeEqual(got, expected);
}));
The predicate runs against (res, req) and serves metrics only when it returns truthy. Async predicates are awaited. Predicate exceptions are caught and treated as denial (no error info leaked back). Denials return 401 Unauthorized with a minimal text body and no metrics payload. The wrapper uses res.cork() + res.onAborted() per uWS best practice so a client hang-up during an async predicate does not write to a dead socket.
Options
| Option | Default | Description |
|---|
prefix | '' | Prefix for all metric names |
mapTopic | identity | Map topic names to bounded label values for cardinality control |
defaultBuckets | [1, 5, 10, 25, 50, 100, 250, 500, 1000] | Default histogram buckets |
maxBuckets | 32 | Reject histogram registration with buckets.length > maxBuckets. Per-histogram override via the 7th positional arg to histogram(). Pass Infinity to disable. |
maxSeries | 10000 | Per-metric labelset cap. New labelsets past the cap are silently dropped, a one-shot console.warn fires per metric, and the built-in prometheus_series_dropped_total{metric=<name>} counter tracks ongoing drops. Per-metric override via the 4th positional arg to counter / gauge (6th to histogram). Pass Infinity to disable. |
Metric names must match [a-zA-Z_:][a-zA-Z0-9_:]* and label names must match [a-zA-Z_][a-zA-Z0-9_]* (no __ prefix). Invalid names throw at registration time. HELP text containing backslashes or newlines is escaped automatically.
Cardinality caps
maxBuckets and maxSeries are defense against unbounded prom-scrape growth. The defaults match mature client libraries (Go’s client_golang warns past ~100 buckets; Java’s Micrometer soft-caps at 64; per-metric labelset state past ~10k usually means the metric is no longer query-able through Grafana).
Histogram bucket cap (maxBuckets, default 32): fires once at registration. The error message names the actual length, the configured cap, and both override knobs so the caller can either narrow the bucket array or opt up explicitly.
Per-metric labelset cap (maxSeries, default 10000): the data-plane action is a no-op rather than throwing, so a high-cardinality bug cannot crash the app on metric increment. Existing labelsets continue to increment past the cap; only new labelsets are dropped. The one-shot warn points at mapTopic as the typical fix when a user-controlled dimension (raw client IP, message id, unbounded topic) is leaking into a label.
The dropped-series counter has maxSeries: Infinity so a high-cardinality drop pattern cannot make the drop counter start dropping. The data-plane cost is one Map.size >= maxSeries comparison per inc / set / dec / observe; unmeasurable on hot paths.
Cardinality control
If your topics are user-generated (e.g. room:abc123), per-topic labels will grow unbounded. Use mapTopic to collapse them:
const metrics = createMetrics({
mapTopic: (topic) => {
if (topic.startsWith('room:')) return 'room:*';
if (topic.startsWith('user:')) return 'user:*';
return topic;
}
});
Metrics reference
Pub/sub bus
| Metric | Type | Description |
|---|
pubsub_messages_relayed_total | counter | Messages relayed to Redis |
pubsub_messages_received_total | counter | Messages received from Redis |
pubsub_echo_suppressed_total | counter | Messages dropped by echo suppression |
pubsub_relay_batch_size | histogram | Relay batch size per flush |
Presence
| Metric | Type | Labels | Description |
|---|
presence_joins_total | counter | topic | Join events |
presence_leaves_total | counter | topic | Leave events |
presence_heartbeats_total | counter | | Heartbeat refresh cycles |
presence_stale_cleaned_total | counter | | Stale entries removed by cleanup (always 0 since extensions 0.5.0 per-field HPEXPIRE replaced application-side cleanup; field kept for back-compat) |
presence_diff_frames_total | counter | topic | diff frames published to topic subscribers. Compared against presence_joins_total + presence_leaves_total it tells you how much per-tick coalescing the buffer is doing. |
presence_diff_coalesced_total | counter | topic | Buffered diff entries overwritten by a later op in the same tick. Non-zero rate confirms same-key collapse is working. |
presence_total_online | gauge | topic | Live online count snapshot. |
presence_heartbeat_latency_ms | gauge | | Most recent heartbeat tick duration. |
presence_keyspace_cleanups_total | counter | | Empty-state events fired by keyspaceNotifications: true on hash expiry. |
presence_joins_aborted_total | counter | topic, reason | presence.join() aborts. Currently reason="ws_closed" only; the label is future-proofed. Added in extensions 0.5.5. |
Replay buffer (Redis and Postgres)
| Metric | Type | Labels | Description |
|---|
replay_publishes_total | counter | topic | Messages published |
replay_messages_replayed_total | counter | topic | Messages replayed to clients |
replay_truncations_total | counter | topic | Truncation events detected |
replay_idmp_hits_total | counter | topic | Idempotency cache hits (duplicate publishId deduped at replay time). |
replay_idmp_writes_total | counter | topic | Idempotency cache writes on fresh publishes. |
replay_replications_total | counter | | Cross-instance replication acks. |
replay_replication_timeouts_total | counter | | Replication acks that timed out. |
Rate limiting
| Metric | Type | Description |
|---|
ratelimit_allowed_total | counter | Requests allowed |
ratelimit_denied_total | counter | Requests denied |
ratelimit_bans_total | counter | Bans applied |
Broadcast groups
| Metric | Type | Labels | Description |
|---|
group_joins_total | counter | group | Join events |
group_joins_rejected_total | counter | group | Joins rejected (full) |
group_leaves_total | counter | group | Leave events |
group_publishes_total | counter | group | Publish events |
Cursor
| Metric | Type | Labels | Description |
|---|
cursor_updates_total | counter | topic | Cursor update calls |
cursor_broadcasts_total | counter | topic | Broadcasts actually sent |
cursor_throttled_total | counter | topic | Updates deferred by throttle |
cursor_attaches_aborted_total | counter | topic, reason | cursor.attach() aborts. Currently reason="ws_closed" only. Added in extensions 0.5.5. |
LISTEN/NOTIFY bridge
| Metric | Type | Labels | Description |
|---|
notify_received_total | counter | channel | Notifications received |
notify_parse_errors_total | counter | channel | Parse failures |
notify_reconnects_total | counter | | Reconnect attempts |
Connection rates
import { wirePublishRateMetrics, connectionMetricsHook } from 'svelte-adapter-uws-extensions/prometheus';
export function init({ platform }) {
wirePublishRateMetrics(platform, metrics);
}
export const close = connectionMetricsHook(metrics);
| Metric | Type | Labels | Description |
|---|
ws_topic_publish_rate | gauge | topic | Per-topic publishes per second (scrape-time). |
ws_topic_publish_bytes | gauge | topic | Per-topic bytes per second (scrape-time). |
ws_connection_* | histogram | | messagesIn, messagesOut, bytesIn, bytesOut, duration per closed connection. |
ws_connection_close_total | counter | code | Per-close-code count. |
connectionMetricsHook(metrics, userClose?) is a close hook factory that records per-connection traffic stats. Pre-existing user close hooks compose by passing them as the second arg.
Cluster-wide publish rate
import { createPublishRateAggregator } from 'svelte-adapter-uws-extensions/redis/publish-rate';
import { wireClusterPublishRateMetrics } from 'svelte-adapter-uws-extensions/prometheus';
const rates = await createPublishRateAggregator(redis);
wireClusterPublishRateMetrics(rates, metrics);
| Metric | Type | Labels | Description |
|---|
cluster_publish_rate_broadcasts_total | counter | | Local rate snapshots broadcast. |
cluster_publish_rate_received_total | counter | | Snapshots received from peers. |
cluster_publish_rate_parse_errors_total | counter | | Malformed snapshots received. |
cluster_publish_rate_instance_count | gauge | | Live peer count. |
Sharded pub/sub bus
| Metric | Type | Labels | Description |
|---|
sharded_pubsub_messages_relayed_total | counter | topic | Messages relayed via SPUBLISH. |
sharded_pubsub_messages_received_total | counter | topic | Messages received via SSUBSCRIBE. |
sharded_pubsub_echo_suppressed_total | counter | | Messages dropped by echo suppression. |
sharded_pubsub_ssubscribes_total | counter | | SSUBSCRIBE calls. |
sharded_pubsub_sunsubscribes_total | counter | | SUNSUBSCRIBE calls. |
sharded_pubsub_parse_errors_total | counter | | Malformed sharded envelopes received. |
Pub/sub bus state
| Metric | Type | Description |
|---|
pubsub_degraded_total | counter | Auto-emitted when the shared circuit breaker trips. |
pubsub_recovered_total | counter | Auto-emitted when the breaker re-closes. |
pubsub_parse_errors_total | counter | Malformed envelopes received. |
Leader election
| Metric | Type | Labels | Description |
|---|
leader_acquired_total | counter | key_class | Successful lease acquisitions. |
leader_lost_total | counter | key_class | Lease losses. |
leader_renewals_total | counter | key_class | Successful lease renewals. |
leader_renewal_failures_total | counter | key_class | Renewal failures. |
Distributed lock
| Metric | Type | Labels | Description |
|---|
lock_acquired_total | counter | key_class | Successful lock acquisitions. |
lock_acquire_wait_ms | histogram | key_class | Time spent waiting for the lock. |
lock_acquire_timeouts_total | counter | key_class | LockAcquireTimeoutError rate. |
lock_lost_total | counter | key_class | Lease lost mid-handler. |
Distributed session
| Metric | Type | Labels | Description |
|---|
session_get_total | counter | result | Hit / miss. |
session_set_total | counter | | Sets. |
session_delete_total | counter | result | Hit / miss on deletes. |
session_touch_total | counter | result | Hit / miss on touches. |
Connection registry / live.push
| Metric | Type | Labels | Description |
|---|
push_requests_total | counter | result | Request outcomes. |
push_reply_latency_ms | histogram | | End-to-end reply latency. |
push_registry_size | gauge | | Live tracked-user count. |
push_late_replies_total | counter | | Replies arriving after timeout. |
push_sends_total | counter | result | Targeted sends. |
push_coalesced_total | counter | result | Coalesced targeted sends. |
push_sendto_total | counter | result | sendTo matches. |
Admission control
| Metric | Type | Labels | Description |
|---|
admission_accepted_total | counter | class | Accepted dispatches. |
admission_rejected_total | counter | class, reason | Rejected dispatches. |
Replay storage
| Metric | Type | Labels | Description |
|---|
replay_storage_fallbacks_total | counter | topic | Best-effort platform.publish fallbacks when localFanoutOnStorageFailure: true. |
Job queue (Postgres)
| Metric | Type | Labels | Description |
|---|
jobs_enqueued_total | counter | queue | Jobs enqueued. |
jobs_claimed_total | counter | queue | Jobs claimed for processing. |
jobs_completed_total | counter | queue | Jobs completed successfully. |
jobs_failed_total | counter | queue | Jobs that failed terminally. |
Task runner (Postgres)
| Metric | Type | Labels | Description |
|---|
tasks_enqueued_total | counter | name | Tasks enqueued. |
tasks_completed_total | counter | name | Tasks completed successfully. |
tasks_failed_total | counter | name | Tasks that failed terminally. |
tasks_retried_total | counter | name | Retry attempts. |
Redis function library
| Metric | Type | Labels | Description |
|---|
redis_function_loads_total | counter | | FUNCTION LOAD calls (library hot-reload). |
redis_function_calls_total | counter | name | Function invocations. |
redis_function_errors_total | counter | name | Function call errors. |
Production assertions
import { wireAssertionMetrics } from 'svelte-adapter-uws-extensions/prometheus';
wireAssertionMetrics(metrics);
| Metric | Type | Labels | Description |
|---|
extensions_assertion_violations_total | counter | category | Critical invariant violations. ~10 categories today. A non-zero rate is a framework bug. |