Skip to content

Relay Reference

Everything a running relay exposes or accepts. For getting one up in the first place, see Self-Hosting the Relay.

All configuration is environment variables. Every one has a working default, and .env.example in the repository is the annotated version of these tables.

Empty strings are treated as unset and fall back to the default. Booleans accept only true or false, and integers must be non-negative. Anything else is a startup error rather than a silent fallback.

VariableDefaultMeaning
PORT8080Port for health, metrics, and the WebSocket upgrade
BIND_ADDRESS0.0.0.0Listen address
WS_PATH/Path the WebSocket upgrade must arrive on
SLOT_ID_PATTERN^([0-9a-f]{32}|[0-9a-f]{64})$Format a slot id must match before rendezvous is attempted
VariableDefaultMeaning
MAX_CONNECTIONS10000Global concurrent connection cap
MAX_UNPAIRED_CONNECTIONS(half of MAX_CONNECTIONS)Ceiling on connections not yet paired, so parked sockets cannot starve pairings that would otherwise succeed. Minimum 2
MAX_CONNECTIONS_PER_IP20Per resolved-IP cap
MAX_CONNECTIONS_PER_SLOT2Per-slot cap
VariableDefaultMeaning
RATE_LIMIT_IP_PER_MIN120New connections per minute, per IP
RATE_LIMIT_IP_BURST40Burst allowance for the above
RATE_LIMIT_SLOT_PER_MIN60New connections per minute, per slot
RATE_LIMIT_SLOT_BURST20Burst allowance for the above

These are cost and abuse controls, not anti-enumeration ones. A previously unseen key starts with a full burst, so an attacker trying a different slot on every attempt never trips the per-slot limiter at all.

VariableDefaultMeaning
MAX_MESSAGE_BYTES1114112Per-message ceiling, 1 MiB plus overhead
MAX_SESSION_BYTES1073741824Total bytes per tunnel before teardown, 1 GiB
MAX_PARKED_BUFFER_BYTES1048576How much an unpaired peer may buffer
MAX_BUFFERED_BYTES16777216Per-connection outbound backlog cap, 16 MiB
VariableDefaultMeaning
PING_INTERVAL_MS30000Ping/pong cadence for reaping half-open sockets
PARK_TIMEOUT_MS60000How long a connection may wait unpaired
MAX_SESSION_MS0Wall-clock cap per tunnel; 0 disables it
SHUTDOWN_GRACE_MS10000Drain budget on shutdown before sockets are terminated
VariableDefaultMeaning
TRUST_PROXYfalseWhether to consult CF-Connecting-IP and X-Forwarded-For
TRUSTED_PROXY_CIDRS(empty)Comma-separated CIDRs. Required when TRUST_PROXY=true
IPV6_PREFIX_BITS64Prefix IPv6 addresses are bucketed to for caps and limits

Setting TRUST_PROXY=true with an empty CIDR list is a startup error, not a warning. See the setting that will stop it booting.

VariableDefaultMeaning
METRICS_ENABLEDtrueWhen false, both metrics endpoints return 404
METRICS_TOKEN(unset)When set, metrics require a matching bearer token
METRICS_ALLOW_UNAUTHENTICATEDfalseServe metrics with no token at all, for a genuinely private deployment
LOG_LEVELinfoOne of error, warn, info, debug
LOG_SLOT_HASHINGtrueConfigures the salted hash that any future slot logging would go through
SLOT_LOG_SALT(random per process)Salt for that hash
VariableDefaultMeaning
ADMISSION_WEBHOOK_URL(unset)Unset means every connection is admitted
ADMISSION_WEBHOOK_TIMEOUT_MS3000Timeout for the admission request
ADMISSION_FAIL_OPENtrueOn webhook error, admit anyway or deny

This is the open-core seam, and the shipped binary does not construct it: setting ADMISSION_WEBHOOK_URL on a stock docker compose up gates nothing. It is a library seam for an embedder. Self-hosting leaves it unset, so every connection is admitted and the relay never talks to any control plane.

EndpointPurpose
GET /healthzLiveness. Always returns 200 while the process is up
GET /readyzReadiness. Returns 503 while draining for shutdown
GET /metricsPrometheus text format
GET /metriczThe same counters as JSON, plus uptime and memory
GET /A static page. WebSocket upgrades arrive on the same path but a different code path

Neither metrics surface ever contains a slot id, an IP address, or frame content. Both answer 404 rather than 401 when no token is configured, so an exposed relay does not advertise that a gated surface exists. /healthz and /readyz are always open and need no token.

Useful when reading logs or debugging a client:

CodeMeaning
4000The peer closed, so this half was torn down too
4403Admission policy denied the connection
4408Stayed unpaired past the park timeout
4409Slot already has two peers
4431Exceeded a buffer or backpressure guard
4432Exceeded the session byte cap
4433Exceeded the session time cap

Before the upgrade completes, failures are plain HTTP instead: 404 for the wrong path, 400 for a bad slot, 429 for rate limits, and 503 when capped or draining.

Three more codes are allocated in the relay’s source but never sent, because each condition resolves before a close frame could carry one. Do not write a client that waits for them, since it would wait forever.

CodeReserved forWhat you actually see
4400Missing or malformed slotHTTP 400, rejected before the upgrade completes, so there is no WebSocket to close
4410A ping went unansweredThe socket is reaped with terminate(), which surfaces as the standard abnormal closure 1006
4503Server is draining for shutdownNew upgrades get HTTP 503; connections already established close with the standard 1001 (going away)

See also:

Kangentic is free and open source. A star helps other people find it.

Star on GitHub