Relay Reference
Everything a running relay exposes or accepts. For getting one up in the first place, see Self-Hosting the Relay.
Environment variables
Section titled “Environment variables”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.
Network
Section titled “Network”| Variable | Default | Meaning |
|---|---|---|
PORT | 8080 | Port for health, metrics, and the WebSocket upgrade |
BIND_ADDRESS | 0.0.0.0 | Listen 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 |
Connection caps
Section titled “Connection caps”| Variable | Default | Meaning |
|---|---|---|
MAX_CONNECTIONS | 10000 | Global 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_IP | 20 | Per resolved-IP cap |
MAX_CONNECTIONS_PER_SLOT | 2 | Per-slot cap |
Rate limits
Section titled “Rate limits”| Variable | Default | Meaning |
|---|---|---|
RATE_LIMIT_IP_PER_MIN | 120 | New connections per minute, per IP |
RATE_LIMIT_IP_BURST | 40 | Burst allowance for the above |
RATE_LIMIT_SLOT_PER_MIN | 60 | New connections per minute, per slot |
RATE_LIMIT_SLOT_BURST | 20 | Burst 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.
Byte caps
Section titled “Byte caps”| Variable | Default | Meaning |
|---|---|---|
MAX_MESSAGE_BYTES | 1114112 | Per-message ceiling, 1 MiB plus overhead |
MAX_SESSION_BYTES | 1073741824 | Total bytes per tunnel before teardown, 1 GiB |
MAX_PARKED_BUFFER_BYTES | 1048576 | How much an unpaired peer may buffer |
MAX_BUFFERED_BYTES | 16777216 | Per-connection outbound backlog cap, 16 MiB |
Lifecycle
Section titled “Lifecycle”| Variable | Default | Meaning |
|---|---|---|
PING_INTERVAL_MS | 30000 | Ping/pong cadence for reaping half-open sockets |
PARK_TIMEOUT_MS | 60000 | How long a connection may wait unpaired |
MAX_SESSION_MS | 0 | Wall-clock cap per tunnel; 0 disables it |
SHUTDOWN_GRACE_MS | 10000 | Drain budget on shutdown before sockets are terminated |
Client IP resolution
Section titled “Client IP resolution”| Variable | Default | Meaning |
|---|---|---|
TRUST_PROXY | false | Whether to consult CF-Connecting-IP and X-Forwarded-For |
TRUSTED_PROXY_CIDRS | (empty) | Comma-separated CIDRs. Required when TRUST_PROXY=true |
IPV6_PREFIX_BITS | 64 | Prefix 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.
Metrics and logging
Section titled “Metrics and logging”| Variable | Default | Meaning |
|---|---|---|
METRICS_ENABLED | true | When false, both metrics endpoints return 404 |
METRICS_TOKEN | (unset) | When set, metrics require a matching bearer token |
METRICS_ALLOW_UNAUTHENTICATED | false | Serve metrics with no token at all, for a genuinely private deployment |
LOG_LEVEL | info | One of error, warn, info, debug |
LOG_SLOT_HASHING | true | Configures the salted hash that any future slot logging would go through |
SLOT_LOG_SALT | (random per process) | Salt for that hash |
Admission
Section titled “Admission”| Variable | Default | Meaning |
|---|---|---|
ADMISSION_WEBHOOK_URL | (unset) | Unset means every connection is admitted |
ADMISSION_WEBHOOK_TIMEOUT_MS | 3000 | Timeout for the admission request |
ADMISSION_FAIL_OPEN | true | On 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.
HTTP endpoints
Section titled “HTTP endpoints”| Endpoint | Purpose |
|---|---|
GET /healthz | Liveness. Always returns 200 while the process is up |
GET /readyz | Readiness. Returns 503 while draining for shutdown |
GET /metrics | Prometheus text format |
GET /metricz | The 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.
Close codes
Section titled “Close codes”Useful when reading logs or debugging a client:
| Code | Meaning |
|---|---|
4000 | The peer closed, so this half was torn down too |
4403 | Admission policy denied the connection |
4408 | Stayed unpaired past the park timeout |
4409 | Slot already has two peers |
4431 | Exceeded a buffer or backpressure guard |
4432 | Exceeded the session byte cap |
4433 | Exceeded 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.
Reserved codes
Section titled “Reserved codes”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.
| Code | Reserved for | What you actually see |
|---|---|---|
4400 | Missing or malformed slot | HTTP 400, rejected before the upgrade completes, so there is no WebSocket to close |
4410 | A ping went unanswered | The socket is reaped with terminate(), which surfaces as the standard abnormal closure 1006 |
4503 | Server is draining for shutdown | New upgrades get HTTP 503; connections already established close with the standard 1001 (going away) |
Next steps
Section titled “Next steps”See also:
- Self-Hosting the Relay - getting an instance running in the first place
- Relay Security Model - why these defaults are what they are
- The Hosted Relay - the instance Kangentic runs, and what it observes
Kangentic is free and open source. A star helps other people find it.
Star on GitHub