Flags & environment#
PacketRelay is configured entirely at startup. Eighteen command-line flags and two environment variables are the whole surface — there is no configuration file, no reload signal, and no runtime tuning endpoint. This page lists every flag with its type, default, valid range and effect, then the two environment variables and the constraints that hold between flags.
The flag table is in source order — the order the fields are declared in
the Args struct at main.rs:143-208, which is also the order --help prints
them. Alphabetical order would separate --max-connections from
--max-connections-per-ip, which are validated against each other.
Flags#
Every default below is the shipped value: all eighteen flags use
default_value_t, so running the binary with no arguments produces exactly
this configuration. Defaults are shown in bold for that reason.
| Flag | Type | Default | Valid range | Effect |
|---|---|---|---|---|
-p, --port |
u16 |
8080 |
Any u16 |
TCP listen port. The bind address is always 0.0.0.0:{port} — main.rs:143-144, main.rs:317 |
--session-ttl |
u64 seconds |
300 |
30–86400 | Maximum age of a stale session before the reaper removes it — main.rs:147-148, session.rs:454 |
--max-connections |
usize |
128 |
1–10000 | Global limiter cap and the size of the legacy-route semaphore — main.rs:151-152, main.rs:297 |
--max-connections-per-ip |
usize |
80 |
1 to --max-connections |
Concurrent connections permitted from one peer IP — main.rs:155-156, rate_limit.rs:92-125 |
--max-connect-rate-per-ip |
usize |
240 |
1–1000000 | New connection attempts per IP per connection-rate window. Rejected attempts count — main.rs:159-160, rate_limit.rs:111-112 |
--connect-rate-window |
u64 seconds |
60 |
1–3600 | Width of the sliding connection-rate window — main.rs:163-164 |
--max-sessions |
usize |
256 |
1–100000 | Global ceiling on the session map, enforced under the creation write lock — main.rs:167-168, session.rs:130-132 |
--max-id-bytes |
usize |
64 |
8–256 | Maximum length in ASCII bytes of session_id and member_id — main.rs:171-172, connection.rs:472-478 |
--hello-timeout |
u64 seconds |
5 |
1–60 | Deadline for the HTTP request head and for the first application hello — main.rs:175-176, main.rs:389 |
--max-message-bytes |
usize |
65536 (64 KiB) |
1024–65536 | Tungstenite max_message_size and max_frame_size. 64 KiB is a hard security ceiling — main.rs:179-180, main.rs:39 |
--max-messages-per-window |
usize |
3000 |
1–1000000 | Per-connection text-frame count budget. Breach sends "message rate exceeded" and closes — main.rs:183-184, rate_limit.rs:220-225 |
--max-bytes-per-window |
usize |
67108864 (64 MiB) |
--max-message-bytes to 1 GiB |
Per-connection payload byte budget. Breach sends "message byte rate exceeded" and closes — main.rs:187-188 |
--message-rate-window |
u64 seconds |
60 |
1–3600 | Width of the fixed message-budget window. Counters reset wholesale — main.rs:191-192, rate_limit.rs:214-219 |
--outbound-queue |
usize |
16 |
1–256 | Depth of the bounded outbound mpsc channel held per connection — main.rs:195-196, connection.rs:167 |
--max-room-members |
usize |
32 |
2–256 | Members permitted in one room. Breach gives "room at capacity" — main.rs:199-200, session.rs:227-256 |
--max-spectators |
usize |
64 |
1–1000 | Spectators permitted in one broadcast. Breach gives "spectator cap reached" — main.rs:203-204, session.rs:208-214 |
--reaper-interval |
u64 seconds |
30 |
1 to --session-ttl |
Cadence of the session and limiter cleanup task — main.rs:207-208, session.rs:454-476 |
--port is the one flag with no documented range beyond its type: any u16
parses, including 0, which asks the operating system for an ephemeral port.
Nothing in Args::validate inspects it — main.rs:213-266.
-p is the only short flag#
--port is declared #[arg(short, long, ...)]; the other seventeen are
declared #[arg(long, ...)] — main.rs:143, main.rs:147-208. There is no
-c for --max-connections, no -t for --session-ttl, and no bundling.
This is worth knowing before writing a start command, because clap rejects an
unknown short flag rather than ignoring it.
Invalid values exit at startup#
validated_args parses, then calls Args::validate, and on any violation
raises a clap ErrorKind::ValueValidation error and exits —
main.rs:212-266, main.rs:269-276. The process never starts with an
out-of-range configuration and never clamps a value into range. The error
message names the flag and its bounds, for example:
--max-message-bytes must be between 1024 and 65536
Type errors are caught earlier, by clap itself: --port banana or
--max-connections -1 fails at parse time before validate runs.
Three flags are validated against another flag#
Fourteen flags carry fixed bounds and --port carries none. The remaining
three are bounded by the value of a second flag, which is what makes them easy
to trip when raising a limit in isolation.
| Constraint | Failure message | Citation |
|---|---|---|
--max-connections-per-ip must be at least 1 and no greater than --max-connections |
--max-connections-per-ip must be between 1 and --max-connections |
main.rs:216-218 |
--max-bytes-per-window must be at least --max-message-bytes, and at most 1 GiB |
--max-bytes-per-window must be at least one max message and at most 1 GiB |
main.rs:240-247 |
--reaper-interval must be at least 1 and no greater than --session-ttl |
--reaper-interval must be between 1 and --session-ttl |
main.rs:262-264 |
Lowering --max-connections below the current
--max-connections-per-ip is the common way to hit the first constraint.
--max-connections 40 alone fails, because the per-IP default of 80 then
exceeds it; both flags have to move together.
The byte-budget constraint exists because a budget smaller than one message would reject every frame the first time it was charged — the connection would be admitted and then closed on its first text frame, which looks like a relay fault rather than a configuration error.
Environment variables#
Exactly two environment variables are read anywhere in the binary.
| Variable | Type | Default | Effect |
|---|---|---|---|
PORT |
u16 |
Unset; falls back to --port |
Overrides --port when present and parseable. A present-but-unparseable value silently falls back to --port — main.rs:313-316 |
RUST_LOG |
tracing-subscriber filter directive |
packet_relay=info |
Sets the log filter. On absence or parse failure the filter is packet_relay=info — main.rs:281-286 |
Both failure paths are silent by design and by consequence. PORT=http does
not stop startup and does not warn — the relay binds --port instead, so a
platform that exports a non-numeric port produces a listener on the wrong port
with a clean log. RUST_LOG=nonsense likewise leaves the default filter in
place, because EnvFilter::try_from_default_env() is followed by
unwrap_or_else rather than an expect.
PORT takes precedence over the flag, not the other way round.
Passing --port 9000 on a host that already exports PORT=8080 yields a
listener on 8080. The env var is checked after argument parsing and wins —
main.rs:313-316.
No flag reads an environment variable#
The clap env feature is not enabled — the dependency is declared with the
derive feature only (Cargo.toml:18), so no #[arg(env = ...)] attribute
exists and none would work. PORT is honoured by an explicit
std::env::var("PORT") call in main, not by clap, and it is the only flag
value that has such a call. There is no PACKET_RELAY_MAX_CONNECTIONS, no
RELAY_SESSION_TTL, and no prefix convention waiting to be discovered.
This matters most in a container, where flags are awkward to pass and environment variables are the native idiom. Every limit other than the port has to be supplied on the command line.
The shipped container passes no flags#
The Dockerfile sets ENV PORT=8080, EXPOSE 8080, USER 65532:65532 and
ENTRYPOINT ["packet-relay"], with no CMD — Dockerfile:1-26. The
image therefore runs on every default in the table above, and railway.json
adds no
startCommand (railway.json:1-11). Changing a limit in a deployed container
means overriding the command, not setting a variable.
packet-relay --max-connections 512 --max-connections-per-ip 64 # together
cargo run --locked -- --port 8080 # from source, all other defaults
What no flag configures#
The eighteen flags above are the complete set. Several things an operator would reasonably look for a flag for have no flag, because the behaviour they would control does not exist in the binary at all.
| Looked-for control | Actual state | Citation |
|---|---|---|
| TLS certificate, key or listener | The relay speaks plain HTTP and WebSocket only. TLS termination is the hosting edge's job, so clients connect to the edge with wss:// |
main.rs:317 |
| Origin allowlist | No Origin header check exists anywhere in the binary |
BACKLOG.md:151-158 |
| Health-check path | /health, /healthz, /ready and /readyz are fixed paths in the route table and are not configurable |
main.rs:474-486 |
| Post-hello idle timeout | None exists. --hello-timeout covers the request head and the first hello only |
connection.rs:333-409 |
| Server-initiated keepalive interval | None exists. The relay echoes Ping as Pong and never sends a Ping |
connection.rs:397-399 |
| Graceful shutdown or drain period | None exists. The accept loop is unconditional and no signal is handled | main.rs:357-381 |
| Persistence, database URL or state directory | Sessions, TOFU pins, nonce high-water marks and limiter state are process memory. A restart loses all of it | session.rs:93, room_auth.rs:141 |
| Bind address | Fixed at 0.0.0.0. Only the port is configurable |
main.rs:317 |
--product-connection-reserve was removed#
The flag was deleted on 2026-08-28 together with the /v1/product-route
handler, its reserved connection pool and the RequestRoute::Product
variant — CHANGELOG.md:10-28. Removed.
The project README.md still lists --product-connection-reserve
with a default of 32, and still describes product-route capacity as additive
and isolated — README.md:160, README.md:177-178. Those rows are stale.
Args has no such field, RouteCapacity holds only the legacy semaphore,
and passing the flag is an unknown-argument error at startup. The code is
authoritative here.
Related#
- Run the relay — building, the container, and the Railway target.
- Limits & capacity — what each cap does under load, and where backpressure bites.
- HTTP endpoints — the health and readiness paths these limits are applied ahead of.
- Message reference — the envelopes the message budget is charged against.
- Security model — which of these limits are the security boundary and which are not.
- Build & test — the toolchain and the gates that pin these defaults.