Skip to content

Heartbeat notifications (dead-man-switch)

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

This is especially convenient for:

  • regular cron jobs (backups, imports, report generators);
  • daemons that “must be always on”;
  • IoT devices that “call home”;
  • batch pipelines where the important thing is not just “success” but “success on time”.
your cron ──ping──▶ POST /heartbeat/ping/<pingToken> (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 — table heartbeats 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 Yandex Cloud timer trigger (cron * * * * ? * — once a 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. “DB backup cron”.
    • Channel — which channel to send the alert through.
    • Interval (sec) — expected period between pings (minimum 30).
    • Grace (sec) — how long to wait after the interval (jitter protection).
    • Alert text — what will arrive in the push when a ping misses the deadline.
    • Alert priority — 0–10, default 8 (loud notification).
    • Recovery text — optional “everything is OK again” message when a ping arrives after an alert.
  3. Copy the Ping URL from the table — your script will hit this URL.
Окно терминала
curl -X POST "$NOTIFLY_URL/heartbeat" \
-H "Content-Type: application/json" \
-H "X-Gotify-Key: <client-token>" \
-d '{
"appid": 12345,
"name": "Cron бэкапа базы",
"intervalSec": 3600,
"graceSec": 300,
"alertTitle": "Бэкап не запустился",
"alertMessage": "За последний час cron бэкапа не пришёл — проверьте сервер!",
"alertPriority": 9,
"recoveryMessage": "Бэкап снова работает."
}'

The response will contain pingToken (starts with H...) and the final URL like https://<domain>/heartbeat/ping/<pingToken>.

If you have a Notifly MCP server configured (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 didn’t run” with priority 9.

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

The most minimal ping is a plain curl without authentication:

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

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

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

The ping is called only on success of the script (thanks to &&). If the backup fails — the ping won’t run, 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 finished 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
okLast ping was within the interval
alertingDeadline missed, alert has already been sent
pausedChecking is disabled (via UI or POST /heartbeat/:id/pause)

After sending an alert the heartbeat does not “spam” — the next check is postponed by 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
GET/POST /heartbeat/ping/:tokenpublicping (token = authentication)

Architecturally, the check “which heartbeats have next_check_at ≤ now” is an indexed 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 a LIST (1 request) + a GET (a request per object), and Object Storage charges per request, and there would be many requests even without alerts. Heartbeats in a project quickly become tens or hundreds — so YDB is noticeably cheaper and faster.