ESP32: push notifications with ntfy.sh, self-hosted
Send push notifications from an ESP32 to your phone with ntfy.sh. One HTTP POST, no cloud account, and the server can live on your own Raspberry Pi.
Every DIY project eventually wants to tell you something. The mailbox opened. The basement is flooding. The plant needs water. Most tutorials route that through a cloud service that wants an account, an API key, and a subscription. ntfy.sh does it with one unauthenticated HTTP POST, and the whole thing is open source, so you can run the server yourself (e.g. on the Raspberry Pi you already have).
This tutorial gets your ESP32 sending phone notifications in about 25 minutes, and shows how to make it fully self-hosted.
What you need
- ESP32 dev board with Wi-Fi
- Your Wi-Fi credentials
- The ntfy app on your phone (App Store or Play Store, free) or just the website ntfy.sh/yourtopic in a browser
No account, no API key, no library. That is the whole pitch.
How ntfy works
Publishing is one HTTP POST. Anything that can send an HTTP request can send a notification: an ESP32, a bash script with curl, a Raspberry Pi cron job, a friend’s server. Subscribing is the topic name. Anyone who knows the topic name can subscribe to it, so pick a topic name nobody will guess (e.g. not “garage”, more like “garage-bx7k2-alerts”).
| Role | What they do |
|---|---|
| Publisher | HTTP POST to a topic URL |
| Subscriber | Listens to the topic in the app |
| Server | Routes the message, public or self-hosted |
The code (public server first)
#include <WiFi.h>
#include <HTTPClient.h>
const char* ssid = "your-wifi";
const char* password = "your-password";
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.print("Connected. IP: ");
Serial.println(WiFi.localIP());
}
void sendNtfy(String title, String message) {
HTTPClient http;
http.begin("https://ntfy.sh/my-garage-bx7k2");
http.addHeader("Content-Type", "text/plain");
// Title header is optional but makes phone notifications readable
http.addHeader("X-Title", title);
int code = http.POST(message);
Serial.print("ntfy POST: ");
Serial.println(code); // 200 = sent
http.end();
}
void loop() {
// e.g. notify when a door sensor fires
if (digitalRead(4) == LOW) {
sendNtfy("Garage door", "Door opened at " + String(millis() / 1000) + "s uptime");
delay(60000); // rate-limit yourself
}
delay(100);
}
Install the ntfy app, subscribe to the topic my-garage-bx7k2, run the
sketch, touch the button wire to ground. Notification on your phone in
about 2 seconds.
Priority, tags, and the headers that matter
ntfy reads its options from HTTP headers:
http.addHeader("X-Title", "Basement water"); // bold title line
http.addHeader("X-Priority", "high"); // 1=min .. 5=max
http.addHeader("X-Tags", "warning,skull"); // emoji tags
Priority high (4) makes the phone buzz even in do-not-disturb on Android. Use it for actual emergencies only (e.g. water on the floor), or you will mute the topic and lose the point.
Self-hosting it, the real setup
The public ntfy.sh is fine to start. The self-hosted version is where this becomes infrastructure you own:
- On a Raspberry Pi (or any always-on box):
sudo apt install ntfyor grab the binary from ntfy.sh/docs/install. - Config lives at
/etc/ntfy/server.yml. The one line that matters:base-url: "http://192.168.1.50:25800"(your Pi’s IP). - Run it:
sudo systemctl enable --now ntfy. - In the phone app: Settings >> Default server, point it at your server’s URL. Subscribe to your topics.
Your ESP32 code changes by one URL: http://192.168.1.50:25800/....
Notifications now flow entirely inside your house. No internet, still
works (the ESP32 and the phone just need to be on the LAN, or on your
VPN when away).
The public server rate-limits per IP and per topic (e.g. burst of 60 messages, then cool-down). For a sensor that alerts every 30 seconds you will hit it fast, and self-hosting fixes it. Also, anything sent to the public server is visible to anyone holding the topic name: keep secrets off it.
When you want ESP32 to RECEIVE too
ntfy also speaks MQTT and WebSockets. On the ESP32 side, subscribing via MQTT with PubSubClient works against a self-hosted ntfy (the public one requires auth for MQTT). The pattern: ESP32 publishes alerts via HTTP POST, your dashboard subscribes via MQTT, one channel does both directions.
What you learned
- One HTTP POST sends a push notification, no account, no library.
- Headers carry title, priority, and tags.
- The public server is a 10-minute start; the self-hosted server is the durable version, and the code change is one URL.
When something breaks
- Notification never arrives: check the POST return code. 200 is sent. 403 means you hit a rate limit. Open ntfy.sh/yourtopic in a browser to see the messages sitting on the server.
- Works from the serial monitor, not from the field: your phone subscribed on the public server but the ESP32 now points at your self-hosted one (or the reverse). The topic has to exist on the same server the publisher uses.
- 2-second delay turning into 30+: the phone app uses a websocket to the server; on mobile networks it can reconnect slowly. Battery optimization settings on the phone app are the usual culprit (e.g. Android likes to sleep it).
- Self-hosted server unreachable from ESP32: they are on different subnets (guest Wi-Fi vs main LAN is the classic). Ping the server from a laptop on the same Wi-Fi as the ESP32 first.
What to build next
- The SMTP email tutorial covers the other self-hosted channel (email when push is not enough).
- The doorbell project sends notifications; ntfy replaces the Pushover dependency.
- A water leak alarm under the sink: two wires, a resistor, and this tutorial. Cheapest insurance in DIY.