Running the relay
Starting and stopping, logs, statistics, hot reloads, multiple instances and large-scale tuning.
Starting and stopping
Start the relay as a background daemon:
nostrfy --config nostrfy.toml start=> nostrfy started (pid 12345)
Or run it in the foreground, in the terminal:
nostrfy --config nostrfy.toml start --foregroundStop it with:
nostrfy --config nostrfy.toml stopVerify it is up:
curl http://127.0.0.1:8080/health=> {"status":"ok"}
Logs and statistics
The daemon writes to daemon.log_file. When the file grows past max_log_size_bytes, it
rotates automatically (nostrfy.log.1, .2, ... up to max_log_files generations):
tail -f nostrfy.logThe log level is controlled by the RUST_LOG environment variable (e.g. RUST_LOG=debug).
Statistics
Live statistics from the CLI:
nostrfy statsOr over HTTP:
curl http://127.0.0.1:8080/relay/statsShows connections, events accepted/rejected, DB size, and more.
Prometheus metrics
curl http://127.0.0.1:8080/metricsHot reload (SIGHUP)
After editing the config file, reload it without a restart:
kill -HUP $(cat nostrfy.pid)| Applies on reload | Requires restart |
|---|---|
| relay identity, public_url | private_key |
| most [limits], reject_ephemeral, enabled_git, enabled_nip78_auth | api_host, metrics_enabled, LiveKit settings |
| — | enabled_nips / disabled_nips, server.host / port / ws_paths |
| — | database.* incl. search_index, blossom.* |
The log warns when a restart-required setting changed.
Running multiple instances
nostrfy supports several independent relays on one server (different ports). Each instance needs its own server.port, [daemon] pid_file/log_file/stats_file (shared values make the second instance refuse to start with already running), database.path, and — when used — its own api_host / blossom.host:
[server]
port = 8080
[database]
path = "/var/lib/nostrfy-a"
[daemon]
pid_file = "/var/run/nostrfy-a.pid"
log_file = "/var/log/nostrfy-a.log"
stats_file = "/var/lib/nostrfy-a/stats.json"
[server]
port = 8081
[database]
path = "/var/lib/nostrfy-b"
[daemon]
pid_file = "/var/run/nostrfy-b.pid"
log_file = "/var/log/nostrfy-b.log"
stats_file = "/var/lib/nostrfy-b/stats.json"Each instance is managed with its own config: nostrfy --config /etc/nostrfy/a.toml start etc.
Large-scale deployments
The relay is designed to scale to hundreds of thousands of connections on a single host — live delivery wakes only the subscribers that can match an event, and per-connection memory is kept small. Pushing into the millions requires host-level tuning:
| Setting | Value | Why |
|---|---|---|
ulimit -n / systemd LimitNOFILE | ≥ 2× the target connections (+1000) | every connection holds an fd |
net.core.somaxconn | ≥ 1024 | pending accept queue for connection bursts |
net.ipv4.tcp_fin_timeout | low (e.g. 10) | reclaims TIME_WAIT sockets faster |
vm.overcommit_memory | 1 or 2 | the LMDB map is a large sparse virtual reservation |
On FreeBSD the same knobs are kern.maxfiles / kern.maxfilesperproc plus ulimit -n, and kern.ipc.somaxconn replaces net.core.somaxconn.
Per-connection kernel memory is ~80 KiB and user space ~10 KiB, so a million connections need roughly 90 GiB of
kernel + user memory on top of the database.
Throughput (events per second)
Event ingestion is bound by two costs: the Schnorr signature check (about 30-50 µs per event) and the synchronous
disk flush the LMDB writer performs after every commit batch. Both are tunable in nostrfy.toml:
| Setting | Why |
|---|---|
database.disabled_fsync = true | commits into the OS page cache (microseconds); a power loss loses only the writes since the last flush — start here |
| CPU cores ≥ 8 vCPU | the batch EVENT path verifies signatures in parallel across cores |
database.search_index = false | drops the NIP-50 word index write per event for write-heavy instances |
The parallel signature check verifies every signature in a pending batch at once on a pool of worker threads (capped at 8; batches under 16 events verify inline). Each event's cheap checks still run first, so the reject reason text is identical to the sequential path — only the Schnorr work is spread across cores. A single-threaded build stays sequential.