Skip to content

Heartbeat notifications (dead-man-switch)

Heartbeat — passive monitoring on the “if it didn’t call, something’s wrong” principle. Your service/cron/script regularly hits a short Notifly HTTP endpoint, and if the next ping doesn’t arrive within the configured time — you receive a push notification with a customizable message.

This is especially convenient for:

  • regular cron jobs (backups, imports, report generators);
  • daemons that “should always be running”;
  • IoT devices that “phone home”;
  • batch pipelines where the important thing is not “success” but “success on time”.
your cron ──ping──▶ POST /heartbeat/ping/<pingToken> (once every N sec)
└─▶ Notifly updates last_ping and shifts next_check_at
every minute: timer-trigger → notifly-heartbeat-checker
└─▶ SELECT WHERE next_check_at <= now AND status IN (ok, pending)
for each: create message + push to WS
  • Storage — the heartbeats table in YDB Serverless. One index on next_check_at — the checker scans only overdue records (cheap).
  • Checking — a separate Cloud Function notifly-heartbeat, triggered by a Yandex Cloud timer (cron * * * * ? * — once per minute).
  • Notification — a normal Notifly message, sent via the channel chosen when creating the heartbeat and delivered to all your clients (web, Android, desktop) like any other push notification.
  1. Open app.notifly.ruHeartbeats.

  2. Click “Create heartbeat”, fill in:

    • Name — for display, e.g. “Database backup cron”.
    • Channel — which channel to use for the alert.
    • Interval (sec) — expected period between pings (minimum 30).
    • Grace (sec) — how long to wait after the interval (protection against jitter).
    • Alert text — what will arrive in the push when a ping misses the deadline.
    • Recovery text — optional “everything’s back to normal” message when a ping arrives after an alert.

    There is no priority field in the form — a heartbeat created via the admin UI has an alert priority of 5 by default. You can set a different priority (alertPriority, 0–10) via the REST API or MCP.

  3. Copy the Ping URL from the table — this is the URL your script will call.

Окно терминала
curl -X POST "$NOTIFLY_URL/heartbeat" \
-H "Content-Type: application/json" \
-H "X-Notifly-Key: <client-token>" \
-d '{
"appid": 12345,
"name": "Cron бэкапа базы",
"intervalSec": 3600,
"graceSec": 300,
"alertTitle": "Бэкап не запустился",
"alertMessage": "За последний час cron бэкапа не пришёл — проверьте сервер!",
"alertPriority": 9,
"recoveryTitle": "Бэкап восстановлен",
"recoveryMessage": "Бэкап снова работает."
}'

The response will include a pingToken (starts with H...) and the final URL like https://<домен>/heartbeat/ping/<pingToken>.

If you have a Notifly MCP server set up (see MCP), just ask the assistant:

Create a heartbeat for the “backups” channel that expects a ping every hour with a 5-minute grace period, and sends “Backup did not run” with priority 9.

MCP tools: list_heartbeats, create_heartbeat, update_heartbeat, pause_heartbeat, resume_heartbeat, delete_heartbeat, ping_heartbeat.

The simplest ping is a plain curl without authorization:

Окно терминала
curl -fsS "$NOTIFLY_URL/heartbeat/ping/<pingToken>" -o /dev/null

The pingToken itself is the authentication. No other headers are needed. Both GET and POST are supported — so you can call it directly from a cron job or monitoring systems that can’t do POST.

*/15 * * * * /usr/local/bin/backup.sh && curl -fsS "$NOTIFLY_URL/heartbeat/ping/H..." -o /dev/null

The ping is triggered only on success of the script (thanks to &&). If the backup fails — the ping won’t happen, and after intervalSec+graceSec an alert will arrive.

In the service unit of a regular timer:

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
ExecStartPost=/usr/bin/curl -fsS https://your-notifly/heartbeat/ping/H... -o /dev/null

ExecStartPost runs only if the main ExecStart completed successfully.

import requests, subprocess
subprocess.check_call(["/usr/local/bin/backup.sh"])
requests.post(f"{NOTIFLY_URL}/heartbeat/ping/{TOKEN}", timeout=10)
import {execSync} from 'child_process';
execSync('/usr/local/bin/backup.sh');
await fetch(`${NOTIFLY_URL}/heartbeat/ping/${TOKEN}`, {method: 'POST'});
StateMeaning
pendingCreated, no pings have arrived yet
okThe last ping is within the interval
alertingDeadline missed, alert has already been sent
pausedChecking is disabled (via UI or POST /heartbeat/:id/pause)

After an alert is sent a heartbeat does not “spam” — the next check is postponed for another intervalSec + graceSec seconds. When the first ping arrives after alerting — the heartbeat returns to ok and (if configured) a recovery notification is sent.

Method & pathAuthorizationPurpose
GET /heartbeatclient-tokenlist heartbeats
POST /heartbeatclient-tokencreate
PUT /heartbeat/:idclient-tokenupdate
DELETE /heartbeat/:idclient-tokendelete
POST /heartbeat/:id/pauseclient-tokenpause checks
POST /heartbeat/:id/resumeclient-tokenresume checks
POST /heartbeat/:id/test-alertclient-tokensend a test alert immediately (status doesn’t change)
POST /heartbeat/:id/test-recoveryclient-tokensend a test recovery immediately (status doesn’t change)
GET/POST /heartbeat/ping/:tokenpublicping (token = authentication)

Architecturally, checking “which heartbeats have next_check_at ≤ now” is an index point-query, not a scan. In YDB Serverless we have INDEX heartbeats_next_check_idx and one short transaction per minute (a few Request Units). In S3 you’d have to do LIST (1 request) + GET (a request per object), and Object Storage charges per request, so there would be many requests even with no alerts. Heartbeats in a project quickly become dozens or hundreds — so YDB is noticeably cheaper and faster.