Tech Digest

Head to head

ntfy vs Gotify

Both turn a shell script into a push notification on your phone for free. They disagree about whether a notification needs an account, and that disagreement plus one missing iOS app decides which you run.

Last reviewed 2 tools compared

ntfy or Gotify?

Use ntfy unless every phone in the house runs Android, in which case Gotify is the tighter fit. ntfy publishes with one line of curl and no account or client registration, and it is the only one of the two with an official iOS app, though a self-hosted instance has to relay a poll request through the public ntfy.sh server to reach Apple's push network. Gotify gives every sender its own revocable application token instead of a guessable topic name and idles around 20 MB, but it has never had a first-party iOS client and there is no hosted Gotify to relay through.

The real choice is between two opposite ideas about what a notification is. ntfy treats it as publish-subscribe over plain HTTP: a topic is just a URL, anyone who knows the string can post to it or read it, no account exists, and a free hosted instance at ntfy.sh will run it for you if you never want to host anything. Gotify treats it as a small self-contained server with real users: you log in, create an application, copy its token, and only holders of tokens send or receive anything.

Both are one Go binary with one SQLite file, both permissively licensed (Apache-2.0 dual licensed with GPL-2.0 for ntfy, MIT for Gotify), both arm64, both ops load 1. The specification table will not pick for you.

Phones will, so say it before anything else: ntfy has an official iOS app and Gotify has never had one, and ntfy.sh exists as a hosted fallback while no public Gotify equivalent does. If anyone in your household carries an iPhone, that pair of facts settles this before you read a single feature comparison.

What the specification table decides, and what it does not#

Two rows repay attention. RAM is 20 MB idle for Gotify against 30 MB for ntfy, a real difference and an irrelevant one on any machine that also runs the services you are alerting about. SSO surprises people: Gotify has native OIDC since v3.1.0, with group claims mapped to user and admin roles, while ntfy has local accounts and per-topic ACLs only, so single sign-on means an auth proxy in front of the web app. Everything else lines up almost exactly.

SpecificationntfyGotify
LicenceApache-2.0 (dual licensed with GPL-2.0) (Permissive)MIT (Permissive)
Written inGoGo / React
First release20212018
MaturityStableStable
DatastoreSQLite (PostgreSQL supported since 2.x)SQLite (MySQL and PostgreSQL supported)
Services to run11
Idle memory30 MB20 MB
Memory in use80 MB50 MB
Operational load1 / 5, Set and forget1 / 5, Set and forget
IdentityLocal accounts onlyNative OIDC
arm64 buildsYesYes
Default ports8080
Backup shapeSQLite backupSQLite backup

iOS is what actually decides this#

Apple only delivers background push through APNs, and your self-hosted server has no APNs certificate. ntfy's documented answer is a relay. Two values in /etc/ntfy/server.yml:

yaml
base-url: "https://ntfy.example.org"
upstream-base-url: "https://ntfy.sh"

Your server then publishes a poll_request containing only a message ID up to ntfy.sh, ntfy.sh wakes the iOS app through Apple's push network, and the app comes back to your server for the body. Set upstream-base-url to https://ntfy.sh and not to your own hostname, which is the usual misconfiguration. Without it, delivery lags 20 to 30 minutes or much longer, which is not alerting.

base-url has to match the URL your clients use exactly, scheme included, because the iOS app compares it against its Default Server setting and the poll topic is derived from it. A mismatch breaks push and attachment links while everything still looks configured, with no error to read.

The privacy cost: the body does not transit ntfy.sh, but the request does, so that operator and Apple both learn a notification happened and when. Timing on a monitoring topic is not nothing. What identifier the upstream sees for your topic is worth checking in the current configuration docs rather than assuming. The maintainer calls the iOS app bare bones, so expect "the alert arrives", not a polished client. Current release 2.28.0, August 2026.

Gotify's position is simpler and worse for iPhones: official Android app, no official iOS app, no hosted instance to relay through. Third-party iOS clients exist and vary. If you need Apple push with nothing touching a third party, neither project gives you that without building and signing your own app against your own APNs certificate.

What a publish call actually looks like#

ntfy puts everything in headers, so the body is the message:

bash
curl -H "Title: Backup failed" \
     -H "Priority: max" \
     -H "Tags: rotating_light,skull" \
     -H "Click: https://kuma.example.org/dashboard" \
     -d "borg exited 2 on nas-01" \
     https://ntfy.example.org/alerts

Priority is min through max (or 1 to 5), tags render as emoji, Click makes the notification open a URL, and Actions adds buttons. Attaching a file is curl -T backup.log -H "Filename: backup.log" https://ntfy.example.org/alerts, bounded by attachment-total-size-limit, which defaults to 5 GB. A sender that can only emit JSON posts {"topic":"alerts","message":"..."} to the server root instead.

Gotify needs a token and a field name:

bash
curl -X POST "https://gotify.example.org/message" \
     -H "X-Gotify-Key: A1b2C3d4E5f6G7" \
     -F "title=Backup failed" \
     -F "message=borg exited 2 on nas-01" \
     -F "priority=8"

Priority is a single integer and higher values are more intrusive on the Android client. There are no tags. A click target means dropping form encoding for JSON with an extras object, which is more than a shell script wants to build:

bash
curl -X POST "https://gotify.example.org/message" \
  -H "X-Gotify-Key: A1b2C3d4E5f6G7" -H "Content-Type: application/json" \
  -d '{"message":"borg exited 2","priority":8,
       "extras":{"client::notification":{"click":{"url":"https://kuma.example.org"}}}}'

Gotify has no file attachments at all; uploaded images are application icons, not message payloads.

For a cron job the difference is whether the script carries a secret. This needs no state anywhere:

cron
0 3 * * * /usr/local/bin/backup.sh || curl -d "backup failed on $(hostname)" https://ntfy.example.org/alerts

The Gotify version needs a token, and the obvious placement is ?token=... in the URL, which puts a permanent credential into shell history, cron mail and every reverse proxy access log you keep. Use the X-Gotify-Key header, and treat a leaked token as a delete-and-recreate job on that application.

Who can read your notifications#

Gotify has no ambient access: every sender holds an application token, every receiver a client token, and applications are deleted individually, so revoking one script does not disturb the others. The cost is friction: every new sender is a trip to the web UI.

ntfy out of the box has no authentication and auth-default-access is read-write. Every topic on your server is world-readable and world-writable. The topic name is the credential, and topic names leak the way URLs leak.

The failure mode is common: someone stands up ntfy behind Caddy on a public hostname, points Uptime Kuma at a topic called alerts or homelab, and never changes the default. Those names are guessable, subscribing costs one GET, and whoever finds it reads every alert about your infrastructure and can publish convincing fake ones back. Nothing warns you. Fix it before the first real message:

yaml
auth-file: /var/lib/ntfy/user.db
auth-default-access: deny-all
enable-signup: false

Then ntfy user add --role=user scripts, ntfy access scripts alerts write, and a token per sender. Set behind-proxy: true too, or every publisher shares one rate limit under the proxy's address. Gotify's equivalent trap is the default account: first run creates admin with password admin, GOTIFY_DEFAULTUSER_* applies only at that first creation, and nothing forces a change.

Which tools already speak each one#

Reach is close to a tie. Uptime Kuma ships providers for both among its roughly 90, Gatus alerts to both from its YAML, Diun lists both as notification backends, and Beszel sends threshold alerts to either. Uptime Kuma vs Gatus and Watchtower vs Diun cover those pairings.

When a tool supports only one, there are three escapes:

  • Generic webhook. ntfy is the easy target, because a raw POST body with no headers is already a valid message. Gotify needs the token in a header or query string and the text in a message field, which some webhook implementations cannot express.
  • Apprise. It carries schemes for both and is bundled in some tools, turning "no provider" into a URL string.
  • Email. ntfy can accept inbound mail as a topic message if you enable its SMTP listener, so an appliance that can only send email still reaches your phone. Gotify has no equivalent.

For anything scriptable the question does not arise. Home Assistant reaches either with a rest_command, Node-RED and n8n with one HTTP request node, and Scrutiny sends disk warnings to a list of notification URLs where a plain webhook entry fills any gap.

The uncomfortable parts#

Your alerting has a single point of failure and it is the alerting. Neither project clusters. A notification server on the same host as the things it watches tells you nothing when that host dies, and silence looks exactly like everything being fine. ntfy's advantage is that the fix is a string: publish the alerts that matter to a topic on hosted ntfy.sh, and they arrive when your rack is off. There is no hosted Gotify to do the same with.

Android push is not one behavior. ntfy's Android client is instant for self-hosted servers because it holds its own long-lived connection in a foreground service instead of routing through Google's push service, which costs a persistent notification and measurable battery. Gotify's app likewise keeps a WebSocket open. Exempt either from vendor battery optimization first, because "notifications stopped working" is usually the OS killing a background socket.

Neither is an archive. ntfy deletes cached messages after 12 hours by default; cache-duration exists so a reconnecting client can catch up, not so you can browse. Gotify keeps messages until an application's message limit trims them, and has no search. If you need to know what alerted three weeks ago, send it somewhere else too.

Which one for your situation#

Your situationUseWhy
Android only, one operatorGotifyTokens instead of guessable names, 20 MB, nothing to tune
Anyone in the house has an iPhonentfyOfficial iOS app; Gotify has none and no relay to borrow
You want zero accounts and a curl one-linerntfycurl -d "done" host/topic with no registration step
You want per-app tokens and clean revocationGotifyOne application token per sender, deleted individually
Behind CGNAT with no public endpointntfyPublish to ntfy.sh, subscribe anywhere, no inbound port
You want alerts to arrive when your server is downntfyHosted ntfy.sh is the same API, so senders move with one string
SSO on the notification web UIGotifyNative OIDC since v3.1.0; ntfy needs an auth proxy
Regulated, nothing may transit a third partyGotify, Android onlyntfy's iOS path always crosses ntfy.sh and APNs
You need searchable historyNeither12 hour cache or per-app limits, no search

What to do next#

If any iPhone is in scope, install ntfy, set base-url and upstream-base-url in the first five minutes, and test from a phone on mobile data rather than home Wi-Fi. If you are Android only and want revocation, install Gotify, change the admin password before exposing it, and create one application per sender.

Either way, do three things next. Lock down access before the first real alert: auth-default-access: deny-all for ntfy, retire the default account for Gotify. Put it behind TLS rather than on port 80, covered in Reverse proxy and TLS, or skip the public endpoint entirely with Remote access without port forwarding. Then give it something worth saying: The minimum viable monitoring stack is the layer that feeds it, and Backups that actually restore is the job whose failure you most want to hear about. The Monitoring category has the rest.

Questions#

Does ntfy work on iPhone with a self-hosted server?

Yes, but not on its own. Your server has no Apple push certificate, so the documented arrangement is upstream-base-url: "https://ntfy.sh" in server.yml. Your server sends a poll_request carrying a message ID to ntfy.sh, ntfy.sh wakes the app through APNs, and the app then fetches the body from your server. Set the value to https://ntfy.sh, not your own hostname, and make sure base-url matches the URL clients use character for character. Without the upstream setting, notifications arrive 20 to 30 minutes late or worse.

Does Gotify have an iOS app?

No, and it never has. The only first-party mobile client is on Android. iPhone users are left with the web UI kept open in a browser or third-party clients of varying quality, and there is no public Gotify instance holding an Apple push certificate that you could relay through the way ntfy.sh does. This is the one part of the decision you cannot configure your way out of afterwards, so check what phones are in the house before you install anything.

Is an ntfy topic private if nobody knows the name?

No. A default install ships with no authentication and auth-default-access set to read-write, so any topic on your server can be read and written by anyone who knows or guesses the name. The topic name is the only secret, and it travels in URLs, cron output, shell history and reverse proxy access logs. Before you point a backup script or a door sensor at it, set auth-default-access: deny-all, create users with ntfy user add, and grant explicit access with ntfy access.

How much RAM does each one use?

Gotify idles near 20 MB and sits around 50 MB in normal use; ntfy idles near 30 MB and around 80 MB. Both are single Go binaries with a SQLite file and one container, and both score 1 on ops load. These are estimates from small installs rather than documented minimums. Neither figure should decide anything: on hardware big enough to run the services you are alerting about, 30 MB of difference is noise next to the iOS question.

Can I run both ntfy and Gotify?

You can, and some households do because their phones disagree, but two notification servers means two places to configure every sender and two things that can be quietly broken. A better version of the same idea is one server plus a second, independent delivery path for the alerts that matter: an SMS, an email, or a hosted ntfy.sh topic your own machine does not serve. The point is not redundancy of software, it is redundancy of the network path.

What happens to my alerts when the notification server itself goes down?

Nothing arrives, and nothing tells you that nothing arrived. Both projects are single instances with no clustering, so a host that dies takes your alerting with it, usually at the exact moment you needed it. ntfy has one advantage here: because a publish is just an HTTP POST to a URL, pointing your most critical senders at a topic on hosted ntfy.sh instead of your own box takes one string change and survives your hardware being off.

Sources#

Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.