Troubleshooting

The errors you are most likely to meet, with step-by-step fixes.

Three things to check first:

  • nostrfy check validates your config (most errors are config mistakes).
  • tail -f nostrfy.log shows the log — the cause is almost always there.
  • nostrfy restart restarts the daemon cleanly.

Cannot start

error: cannot bind to 0.0.0.0:80: Permission denied

Cause: Port 80 can only be bound by root.

Fix: Run with sudo, or change the port to something like 8080.

sh
# Change port = 8080 in the config file, then:
nostrfy --config nostrfy.toml start

error: cannot bind to ...: Address already in use

Cause: Another process (an old nostrfy or a different server) is already using the port.

Fix:

sh
ss -tlnp | grep :8080
sh
# If nostrfy is running, restart it
nostrfy --config nostrfy.toml restart

already running (pid 1234); use 'nostrfy stop' or 'nostrfy restart'

Cause: nostrfy is already running; start refuses to start a second instance.

Fix: Use nostrfy restart, or just use the running instance.

nostrfy stop hangs / did not stop in time

Cause: The daemon is stuck or not responding.

Fix:

sh
ps aux | grep nostrfy
kill -9 <PID>
# Remove a stale pid file if present
rm -f nostrfy.pid

error: invalid nostrfy.toml: TOML parse error

Cause: The config file is not valid TOML. Common mistakes: forgetting quotes around a string, or writing the same key twice.

Fix: The error message includes a line number. Check and fix that line.

toml
# Correct examples
name = "my relay"        # strings are quoted with "
port = 8080              # numbers are plain
enabled_nips = [1, 50]   # lists are wrapped in [ ]

error: cannot read nostrfy.toml: No such file or directory

Cause: The config file does not exist.

Fix:

sh
nostrfy --config nostrfy.toml init

error: relay.private_key is not a valid secp256k1 secret key

Cause: relay.private_key is not a valid 64-character hex key.

Fix: Run nostrfy genkey to generate a correct key (or set private_key = "").

Lots of warnings in the log at startup

[WARN] log lines tell you about configuration problems. The main ones:

WarningMeaning and fix
relay.public_url is empty and server.host is "0.0.0.0"...public_url is not set — NIP-42 auth, NIP-62 vanish and NIP-98 admin auth will not work. Set wss://your-public-url.
relay.private_key is empty while NIP-29 is enabled...Groups need a secret key. Run nostrfy genkey.
unknown config key [relay].software is ignoredAn unused legacy key (or a typo) in the config. Check the key name.
unknown config section [serve] is ignoredA typo in a section name (e.g. [serve] instead of [server]). Fix it.
relay.require_auth is true but relay.send_auth_challenge is false...This combination locks everyone out. Change one of the two.
relay.require_pow = 64 ... practically unmineableThe PoW requirement is so high nobody can post. Lower require_pow.

Cannot connect or behaves strangely

Client gets connection refused

Cause: The relay is not running, or a firewall is blocking the port.

Fix:

sh
curl http://127.0.0.1:8080/health

# From outside (using the server's IP/port)
curl http://YOUR_SERVER_IP:8080/health

# Check the firewall (example: ufw)
sudo ufw status
# Open the port if needed
sudo ufw allow 8080

External clients cannot connect, local ones can

Cause: server.host is still 127.0.0.1 (the default), which only accepts local connections.

Fix: Set host = "0.0.0.0" in the config and restart.

Cannot connect through a Cloudflare tunnel

When using Cloudflare Tunnel:

  • The relay runs plain HTTP; Cloudflare terminates TLS, so clients use wss://. Set public_url = "wss://..." on the relay (this makes NIP-42 auth work).
  • Cloudflare adds an X-Forwarded-Proto header. nostrfy treats ws/wss/http/https values the same, so no extra configuration is normally needed.

error: message too large and the connection closes

Cause: A single message exceeds max_ws_message_bytes (default 1 MB).

Fix: Raise limits.max_ws_message_bytes if you need larger events — but also check the client's own limits.

too many subscriptions / too many filters errors

Cause: The per-connection caps were reached (subscriptions default 20, filters default 20).

Fix: Raise limits.max_subscriptions / limits.max_filters (and check the client settings).

New connections are refused under load

Cause: max_connections (default 10000) was reached, the per-IP cap (max_connections_per_ip, default 64) kicked in, or the per-second connection rate limit (max_connections_per_sec_per_ip) refused the burst. The caps apply to every connection — WebSocket and plain HTTP alike.

Fix: Review and adjust the settings. max_connections_per_ip = 0 disables the per-IP cap; max_connections_per_sec_per_ip = 0 disables the rate limit. These three settings require a restart.

Connections drop after a while

Cause: If ws_idle_timeout_secs is set, idle connections are closed. Healthy clients answer the relay's PING with a PONG and stay connected; only dead peers are reaped.

Fix: This is intentional — the default is 300 seconds. Set ws_idle_timeout_secs = 0 to disable it entirely.

A subscription ends with CLOSED ... response too large

Cause: The stored events of one REQ exceeded max_req_response_bytes (default 32 MiB). Only happens with very large events or very wide filters.

Fix: Narrow the filter (tighter since/until, a lower limit) or raise max_req_response_bytes (0 disables the budget).

A NIP is missing from the NIP-11 supported_nips list

Cause: The advertised list is dynamic — a NIP is hidden when all the kinds it defines are rejected: they are all in blocked_kinds, none of them is in allowed_kinds, or they are ephemeral kinds rejected by reject_ephemeral. NIP-29/43/66 additionally require relay.private_key and NIP-86 requires rpc.management_token or rpc.admin_pubkey.

Fix: Check the active access lists — NIP-86 listallowedkinds shows the kind allowlist, and GET / shows the effective supported_nips immediately. Remove the blocking kind or the reject_ephemeral setting.

Errors when publishing

When publishing fails, the 4th element of the OK message explains why. The common ones:

ErrorMeaning and fix
invalid: signature verification failedThe event signature is invalid (possibly a broken client key).
invalid: content too largeContent exceeds max_content_bytes (default 64K characters). Shorten it or raise the limit.
invalid: too many tagsMore tags than max_tags (default 2000).
invalid: event creation date is in the futureTimestamp too far in the future (beyond max_created_at_future_secs).
mute: event contains secret key materialThe content or tags contain an nsec-looking string. Never post secret keys. Remove the string and the event is accepted.
duplicate: event already storedThe same event is already stored (normal).
blocked: pubkey not allowedThe pubkey is banned (banpubkey) or outside the allowlist.
blocked: kind not allowedThis kind is disallowed.
rate-limited: too many eventsThe pubkey exceeded max_events_per_min_per_pubkey (sliding 60-second window). Wait a minute and retry, or raise/disable the limit.
blocked: event has been bannedThe event id is banned.
blocked: event has been deletedRe-publishing a deleted event.
auth-required: ...Authentication is required (when relay.require_auth is on).
restricted: your account is too newThe account was created within new_pubkey_min_age_secs. Wait and retry.
restricted: unknown groupThe group does not exist (create it first).
restricted: this group is closedThe group is closed; join requests without an invite code are not honored.

Blossom file server

Upload fails with 401

The upload authorization event (kind 24242) was rejected. Check that:

  • the token's expiration tag is present and set to a unix timestamp in the future,
  • for upload/media/delete the token carries an x tag with the blob's sha256,
  • the server tag (when present) names exactly the configured blossom.host (hostname only, no scheme/path),
  • the token was signed within the last 10 minutes (a freshness window against replay),
  • and the signing key is the uploader's own.

Upload fails with 403

blossom.restrict_uploads = true is set and the pubkey is not on the allowlist — add it with nostrfy blossom allow npub1... (the daemon reloads automatically). If the list looks wrong, nostrfy blossom list shows it.

Upload fails with 409

The client sent an X-SHA-256 header that does not match the actual request body (the declared hash was computed over different bytes — e.g. the file changed between hashing and sending). Clients may omit the header entirely.

GET / on the media host serves the NIP-11 document

The request did not reach the relay with the Blossom Host header. Point media.example.com (or whatever blossom.host is set to) at the same port in the reverse proxy, then nostrfy restart.

A blob 404s right after upload

The file is content-addressed by its SHA-256: fetch it via the exact hash returned in the upload response (/<sha256> or /<sha256>.<ext>). A mismatch means the client requested a different hash than the bytes it sent.

Search, groups and auth

Search returns 0 results / unexpected results

nostrfy search matches whole words. Note that:

  • search = "rust" matches events containing the word "rust", but "ru" does NOT match "rust" as a substring.
  • Only words in the event content are searched.
  • If search_index = false, search still works but is slower.
  • If NIP-50 is disabled (disabled_nips = [50]), search is ignored (a NOTICE is sent).

Group metadata (39000-39005) is not generated

Cause: relay.private_key is not set. Group snapshots are signed by the relay's own key, so without it nothing is generated.

Fix:

sh
nostrfy --config nostrfy.toml genkey
nostrfy --config nostrfy.toml restart

restricted: unknown group rejects group events

Cause: The group does not exist. In NIP-29, moderation events and join requests (9021) cannot target a group before it is created (kind 9007).

Fix: Create the group with a 9007 event first.

restricted: you are not an admin of this group

Cause: Moderation (adding members, etc.) requires an admin (a member with a role). The creator is an admin.

Fix: Ask an admin to grant you a role, or create your own group.

restricted: this group is closed

Cause: The group is closed; join requests without an invite code are not auto-approved.

Fix: Ask an admin for an invite code (9009) and join with a code tag.

Accidentally left a group, or the group has no admins

Cause: NIP-29 leave requests (kind 9022) are honored for any member — including the group's last admin, who leaves no admins behind. With no admin, nobody can send moderation events (9000/9001/9002/9008) anymore.

Fix: Sign a moderation event with the relay's own key (relay.private_key, the pubkey advertised as NIP-11 self). Per NIP-29, moderation events may come from "the relay master key or ... group admins", so the relay accepts group moderation signed by its own key even when the group has no admins. For example, restore an admin with a kind:9000:

json
{
  "kind": 9000,
  "pubkey": "<relay self pubkey>",
  "tags": [["h", "<group-id>"], ["p", "<member-hex>", "admin"]]
}

Sign and publish it with the relay key. Alternatively, delete the group with a relay-signed kind:9008 (its stored events are purged) and re-create it with kind:9007. This recovery needs relay.private_key to be configured.

Protected events are rejected with auth-required

Cause: NIP-70 protected events (with a - tag) may only be published by the authenticated author on the same connection.

Fix: Enable NIP-42 auth in the client before publishing.

AUTH (NIP-42) returns false

Common causes:

  1. relay.public_url is unset or wrong — the AUTH event's relay tag does not match the relay's URL. Set wss://... and restart.
  2. Stale challenge — you sent AUTH on a different connection, or reused an old challenge.
  3. The client clock is off — the AUTH event's created_at must be within ±10 minutes of now.

NIP-86 management API returns 401 unauthorized

Cause: Missing or wrong credentials.

Fix:

  • Set management_token and send Authorization: Bearer <token>.
  • Or set admin_pubkey and send a NIP-98 auth event (the u tag must match the relay URL exactly; a payload tag is required).
  • If neither is set, the management API is disabled entirely.

NIP-98 auth events are rejected for a different scheme or port

The NIP-98 spec says the u tag must be exactly the same as the absolute request URL, so nostrfy derives the expected URL from relay.public_url: its authority plus the HTTP scheme mapped from the WebSocket scheme (wss://https://, ws://http://, nostr+ stripped). Without public_url the relay expects the plain http://host:port it serves. A tag with another scheme, a different/omitted port, or a different path or query is rejected — set relay.public_url to the public address clients sign. Each auth event is also single-use: replaying the same Authorization header within its 60-second validity window is refused.

Database and disk

database map is full: increase database.max_map_size

Cause: The LMDB memory-map ceiling (default 1 TB of virtual address space; actual disk usage grows with data) was reached — effectively, the database is full.

Fix: Raise database.max_map_size and restart.

disk is full: refusing to commit N events

Cause: Less than 32 MB of free disk space. Writes stop (to protect the data); reads continue.

Fix: Free up disk space. Writes resume automatically once space is available. (df -h /path/to/data)

nostrfy check reports map_size must not exceed max_map_size

Cause: database.map_size is larger than max_map_size.

Fix: Set map_size at or below max_map_size (the defaults are fine).

Checking the database size

sh
curl http://127.0.0.1:8080/relay/stats
# => "db_size_bytes" in bytes

Backing up / moving the database

All data lives in the database.path directory. Stop the relay before copying (copying a live database can corrupt it).

sh
nostrfy --config nostrfy.toml stop
cp -a ./data ./data-backup
# Also back up [blossom].local_path when using local Blossom storage.
nostrfy --config nostrfy.toml start

Daemon operation

nostrfy stats says nostrfy is not running (no stats file)

Cause: The stats file does not exist — the daemon is not running, or it started less than a few seconds ago.

Fix: Run nostrfy start, wait a few seconds, and try again.

The log grows without bound

Cause: max_log_size_bytes is 0 (rotation disabled).

Fix: Set max_log_size_bytes = 52428800 (50 MB) and max_log_files = 5. Rotation is automatic.

Changes to the config do not take effect after reload

Cause: You reloaded (SIGHUP) settings that are fixed at startup: private_key, api_host, metrics_enabled, LiveKit settings, and the NIP enable/disable lists.

Fix: Use nostrfy restart. The log contains a "a restart is required" warning in this case.

The relay keeps dying by itself

Cause: The machine rebooted, or the relay ran out of memory (OOM).

Fix:

  1. Check the end of the log: tail -50 nostrfy.log.
  2. Check if the machine rebooted: uptime (a very short uptime means a reboot).
  3. Check memory: free -h.
  4. Start the relay again: nostrfy start.
Tip
To start nostrfy automatically on boot, register it as a systemd service with the relay's start command as ExecStart.

systemd cannot start the relay on port 80

A systemd service running as root can bind port 80. If you set User= to a regular user, either use a higher port (e.g. 8080) or add AmbientCapabilities=CAP_NET_BIND_SERVICE to the unit.

Still not solved?

  1. Check the log: tail -100 nostrfy.log — it usually names the direct cause.
  2. Re-validate the config: nostrfy check — shows warnings and errors.
  3. Gather reproduction details: what were you doing, which client, what exact error.
  4. Ask in the project repository: https://github.com/iqbqioza/nostrfy — when filing an issue, include the reproduction steps and the log.