Skip to content

Troubleshooting

Scope: failures, matched to the evidence for each. The server explains itself in the error JSON of chapter 3 — a code, an HTTP status, and a sentence — so most diagnosis starts by reading the answer instead of guessing at it.

Symptom: publishing returns HTTP 403, "forbidden"

Cause: the server knows who is asking (or that nobody is) and the answer is no: anonymous publishing on a deny-all server, a user publishing to a topic they have no write grant on, or a subscription reading a topic without a read grant. Chapter 8 captured all three shapes.

Fix: check the grants (ntfy access, with the config pointer from chapter 8), check which user the request authenticated as — and if you expected an open server, check auth-default-access in the config the server actually started with.

Symptom: publishing returns HTTP 401, "unauthorized"

Cause: credentials were offered and not believed — a wrong password, a misspelt user, or credentials sent in a form the client mangled. 401 is about identity; 403 is about permission. A wrong password never becomes right by adding grants.

Fix: retype the password; try the same request interactively before blaming the script; if the user was just created, confirm which auth database the user command edited — the next symptom is that trap.

Symptom: ntfy user says the auth database is not configured, or new users have no effect

Cause: the user and access commands edit an auth file they find through the server config — and, in 2.x, --config is not a global flag, so pointing them at your config the obvious way fails:

$ ntfy --config server.yml user add alice
Incorrect Usage: flag provided but not defined: -config

Fix: point with the environment variable (or the --config flag those commands carry themselves), from chapter 8:

export NTFY_CONFIG_FILE=/path/to/server.yml
ntfy user list

Check: user list shows the users you expect before you change anything — including the * anonymous line with the server-config default you intended.

Symptom: a subscriber sees nothing

Cause: the usual four, in order. The topic name differs by one character between publisher and subscriber (topics are exact strings, and each is its own channel). The messages were published before the subscription opened, and sit in the cache — use chapter 5's poll and since. The message was published with a delay that has not yet passed (chapter 6). Or the client is buffering: curl without -N holds streamed lines back, and the subscription looks dead while working perfectly.

Fix: publish one message from a second shell while the subscriber is open, and watch both sides — the publisher's receipt proves the server has it; the cache poll proves where it went.

Symptom: yesterday's messages are gone

Cause: they expired. The cache keeps messages 12 hours by default, attachments 3; both numbers are visible in any receipt's expires field, and both are config settings (cache-duration, attachment-expiry-duration), not malfunctions.

Fix: if a message must outlive its notification, the subscriber keeps it — chapter 9's log-file recipe. Raising cache-duration makes the server an archive it was not built to be; do it knowingly, or not at all.

Symptom: a long message arrived as a file

Cause: the request body crossed the 4,096-byte boundary measured in chapter 6, and the server — with an attachment cache configured — filed the body as an upload and made the message a note about it.

Fix: send long content as an attachment on purpose, with a short message of your own writing; or carry the text in a field (JSON, or the message parameter) instead of the bare body.

Symptom: the server will not start, or the address does not answer

Cause: the mundane trio: another process already holds the port; the config file is not where --config points (the startup log says what it read); or the client is asking a different address than the server is listening on — listen-http is an exact host:port, and 127.0.0.1 is not reachable from other machines, ever, by design.

Fix: read the first line of the server's log (Listening on …), confirm the health endpoint on the same address, and only then debug the network between.

Symptom: the phone does not buzz

Cause: outside the lab's reach, and this guide says so plainly: phone delivery depends on the app, the phone's battery and notification settings, and — on iOS — Apple's push infrastructure, none of which a local server capture can settle. What can be settled from a desk: the message reached the server (the publisher's receipt) and reached a desktop subscriber (chapter 4) — if both are true, the loss is on the phone's side of the pipe.

Fix: check the app's subscription is to the same server and topic; check the topic's messages in the web app; then the phone's per-app notification and battery settings. The upstream documentation's notes on instant delivery are the reference here — this symptom is listed as documented-only in criteria.md.