Configuration
moq-relay is configured via a TOML file. Pass the path as the only positional argument:
moq-relay relay.tomlMinimal Example
[server]
listen = "0.0.0.0:4443"
[server.tls]
cert = "cert.pem"
key = "key.pem"Full Reference
[log]
Logging configuration.
[log]
# Log level: trace, debug, info, warn, error
# The RUST_LOG environment variable takes precedence
level = "info"[server]
QUIC/WebTransport server settings. Optionally add plaintext qmux stream listeners for trusted local workers. Every connection authenticates through the same JWT / public-access path; QUIC additionally accepts an mTLS client certificate, and Unix sockets add optional peer-credential gating.
[server]
# QUIC (UDP) bind. Omit to run stream-only (no QUIC) when a tcp/unix listener
# is configured below.
bind = "[::]:443"
# Plaintext qmux over TCP (no TLS, carries no peer identity). Trusted networks
# only; a non-loopback bind logs a warning. Requires the `tcp` build feature.
[server.tcp]
bind = "127.0.0.1:4444"
# Plaintext qmux over a Unix socket, for local workers (e.g. the protocol
# gateways or a stats publisher). Requires the `uds` build feature. Restrict
# callers by peer credentials (each list AND across, OR within; empty = no
# constraint).
[server.unix]
bind = "/run/moq/internal.sock"
[server.unix.allow]
uid = [1001]
# gid = [2000]
# pid = [12345]No-JWT connections on the stream transports resolve through the same public-access rules as tokenless QUIC clients (see [auth] public). See Stream Listeners for details.
[server.tls]
TLS configuration for the QUIC endpoint.
[server.tls]
# Option 1: Provide certificate files
cert = "/path/to/cert.pem" # Certificate chain
key = "/path/to/key.pem" # Private key
# Option 2: Generate self-signed certificates (development only)
generate = ["localhost", "127.0.0.1"]
# Optional: root CAs to accept for mTLS peer authentication.
# Clients that present a cert signed by one of these CAs are granted
# full access (publish/subscribe/cluster). Intended for relay clustering.
# Supported by the quinn and noq backends.
root = ["/path/to/peer-ca.pem"]For production, use certificates from Let's Encrypt or another CA.
[web.http]
HTTP server for debugging endpoints.
[web.http]
# Listen address for HTTP (TCP)
# Defaults to disabled if not specified
listen = "0.0.0.0:4443"See HTTP Endpoints for available endpoints.
[web.https]
HTTPS/WSS server for TCP fallback.
[web.https]
# Listen address for HTTPS/WSS (TCP)
listen = "0.0.0.0:443"
# TLS certificates (can be the same as server.tls)
cert = "cert.pem"
key = "key.pem"[auth]
Authentication configuration.
[auth]
# Path to the JWT verification key
# - Symmetric: the shared secret key
# - Asymmetric: the public key
key = "root.jwk"
# Path prefix for anonymous access
# Omit to require authentication everywhere
public = "anon"See Authentication for details on token generation.
[cluster]
Clustering configuration for multi-relay deployments.
[cluster]
# Peers this relay dials, as full URLs. The topology is whatever you draw with
# these links. A JWT may be supplied inline as a ?jwt= query parameter. A bare
# host or "host:port" is deprecated but still accepted (wrapped in https://.../).
connect = ["https://us-east.example.com/?jwt=..."]
# Optional. This relay's own externally-reachable URL (identity). Advertised to
# peers when gossip is on, and sent to connect_api as ?node=.
node = "us-west.example.com:4443"
# Optional. Enable gossip discovery: advertise `node` so peers find you
# automatically. Boolean; requires `node` to be set.
mesh = true
# Optional. Fetch the peer list from an HTTP(S) endpoint or local file (a JSON
# array of hostnames) and reconcile it at runtime, no restart needed.
connect_api = "https://api.example.com/cluster/connect"
# JWT for outbound cluster dials (alternative to mTLS), applied to any peer
# whose URL has no inline ?jwt=. Required to authenticate gossip / connect_api
# discovered peers; for static `connect` peers, prefer an inline ?jwt=.
token = "cluster.jwt"See Clustering for topology choices and the trade-off between hand-listed peers and gossip.
[client]
Client settings used when connecting to other relays (clustering).
[client]
# Disable TLS verification (development only!)
tls.disable_verify = true
# Or provide trusted root certificates. By default these replace the system
# roots, so the relay trusts only these CAs.
# tls.root = ["/path/to/root.pem"]
# Set this to also trust the platform's system roots alongside any custom root,
# e.g. to dial a local relay with a private CA and a remote one with a public CA.
# Defaults to true only when no custom root is set.
# tls.system_roots = true[stats]
Per-node stats publishing. When enabled, the relay publishes stats broadcasts carrying JSON snapshots of the broadcasts it's currently serving and of the sessions currently connected to it. By default, it publishes a single <prefix>/node/<node> broadcast (or <prefix>/node when node is unset). Set depth to bucket stats by the first N broadcast path segments and publish one broadcast per bucket at <prefix>/<bucket>/node/<node>.
[stats]
# Master switch (defaults to false)
enabled = true
# Top-level path under which stats broadcasts are published (defaults to ".stats")
prefix = ".stats"
# Seconds between snapshot publishes (defaults to 1)
interval = 1
# Node identifier appended to the advertised path to disambiguate broadcasts
# when multiple relays share a cluster origin. May be multi-segment, e.g.
# "sjc/1" / "sjc/2" for two hosts nested under a shared region key.
# Single-relay deployments can omit this.
node = "sjc/1"
# Number of leading broadcast path segments to bucket stats by (defaults to 0).
# Set to 1 for one stats broadcast per first path segment, e.g. per tenant.
depth = 1Each stats broadcast carries four per-broadcast tracks, one per (tier, role) pair, plus two session tracks (one per tier):
| Track | What it covers |
|---|---|
publisher.json | external (e.g. customer) egress |
subscriber.json | external ingress |
internal/publisher.json | internal (e.g. mTLS cluster peer) egress |
internal/subscriber.json | internal ingress |
sessions.json | external connected sessions, keyed by root |
internal/sessions.json | internal connected sessions, keyed by root |
Each per-broadcast frame is a JSON object mapping broadcast path to a cumulative counter snapshot. An entry surfaces on any tick where the broadcast is live (any open counter still exceeds its *_closed counterpart, so a subscription could begin at any moment) or its snapshot changed since the previous tick. Once every counter equals its *_closed counterpart no traffic can flow, so the entry is dropped:
{
"demo/bbb": {
"announced": 1, "announced_closed": 0, "announced_bytes": 8,
"broadcasts": 1, "broadcasts_closed": 0,
"subscriptions": 5, "subscriptions_closed": 2,
"bytes": 12345, "frames": 678, "groups": 9
},
"anon/foo": {
"announced": 1, "announced_closed": 0, "announced_bytes": 8,
"broadcasts": 1, "broadcasts_closed": 0,
"subscriptions": 2, "subscriptions_closed": 0,
"bytes": 234, "frames": 12, "groups": 1
}
}Field semantics:
announced/announced_closed: cumulative count of every broadcast announce/unannounce event on this(tier, role)slot, regardless of whether any subscription happened. Use this for "all known broadcasts".announced_bytes: cumulative broadcast-name length summed over each announce and unannounce of this broadcast. It counts the name, not the encoded message size, so a broadcast isn't charged for hop chains or framing overhead (and the count is the same across protocol versions). Separate frombytes, which is media payload.broadcasts/broadcasts_closed: per-(broadcast, session) subscription sentinel. The first active subscription a peer session opens for a broadcast bumpsbroadcasts; the last one it closes bumpsbroadcasts_closed. Summed across sessions,broadcasts - broadcasts_closedis the number of distinct sessions currently subscribed to the broadcast (i.e. viewers on the egress side), which is typically what billing and UI want.subscriptions/subscriptions_closed: cumulative count of track-level subscription guards opened and dropped.bytes/frames/groups: cumulative payload counters from the session loops (both themoq-liteand IETFmoq-transportpaths).
The session tracks (sessions.json, internal/sessions.json) instead map each auth root to a { sessions, sessions_closed } snapshot. sessions bumps when a session authenticated under that root connects and sessions_closed when it disconnects, so sessions - sessions_closed is the number of sessions currently connected under the root. This counts presence regardless of whether any data flows, so a client connected to e.g. /acme is billable even while idle. A root entry is emitted while live or on the tick it changed, then dropped once no session under it remains:
{
"acme": { "sessions": 3, "sessions_closed": 1 },
"globex": { "sessions": 1, "sessions_closed": 0 }
}Tier, role, and node are implied by the track and broadcast paths, so they aren't repeated inside the frame. Counters are cumulative and strictly monotonic; a counter going backwards across successive snapshots means the underlying entry was garbage-collected and re-created (relay restart or a long idle gap). Downstream consumers should treat decreases as a fresh session segment and sum across resets when computing lifetime totals.
Each snapshot reads *_closed atomics before their open counterparts, which guarantees the emitted snapshot never shows closed > open even under concurrent bumps (it can momentarily show an inflated open count, which is logically valid).
Frames for any one (tier, role) are skipped when the JSON is byte-identical to the last emitted frame; new subscribers still pick up a baseline immediately via track-latest semantics.
Every flag also accepts an equivalent CLI argument (--stats-enabled, --stats-prefix, --stats-interval, --stats-node, --stats-depth) and environment variable (MOQ_STATS_ENABLED, MOQ_STATS_PREFIX, MOQ_STATS_INTERVAL, MOQ_STATS_NODE, MOQ_STATS_DEPTH).
[iroh]
Experimental P2P support via iroh.
[iroh]
# Enable iroh for P2P connections
enabled = false
# Path to persist the iroh secret key
secret = "./relay-iroh-secret.key"Example Configurations
See the demo/relay/ directory for working configuration files:
- Development -
demo/relay/root.toml - Production -
demo/relay/prod.toml - Cluster Leaf Node -
demo/relay/leaf0.toml
Environment Variables
RUST_LOG- Override the log level (e.g.,RUST_LOG=debug)MOQ_IROH_SECRET- Set the iroh secret key directly
See Also
- Authentication - JWT setup
- HTTP Endpoints - Debug endpoints
- Clustering - Multi-relay deployments
- Production Deployment - Production checklist