← All heartbeat examples · What Heartbeat is
Heartbeat for machines nothing can reach from outside
Every uptime monitor works from the outside in: it needs a stable address and an open port to probe. A home server behind carrier-grade NAT has neither. Nor does the Raspberry Pi in the garage, the NAS, the laptop running a week-long job, or the box in a client's rack on a network you do not control. A heartbeat is the other way round: the machine makes one outbound HTTPS request a minute, and anything that can open an outbound connection can be watched. No port is opened, no agent is installed, no account is created on the box. The only thing that lives there is the slot's URL.
OFF /
ON lines in the slot's note are the only account of
what happened, written at the time by a third party into a note nobody can edit. Wi-Fi drops and ISP outages
show up as gaps too, which is honest: the record describes the machine's reachability, not just its power.
1. Linux: Raspberry Pi, home server, NAS, client box
A systemd timer beats a crontab here because of OnBootSec:
the first ping goes out 30 seconds after boot, so a reboot that finishes in under two minutes writes nothing
and a longer one is an exact OFF /
ON pair. The URL lives in a root-only file.
# /etc/heartbeat.env (chmod 600)
HEARTBEAT_URL=https://freshjots.com/hb/<token>
# /etc/systemd/system/heartbeat.service
[Unit]
Description=Fresh Jots heartbeat ping
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
EnvironmentFile=/etc/heartbeat.env
ExecStart=/usr/bin/curl -fsS --max-time 10 ${HEARTBEAT_URL}
# /etc/systemd/system/heartbeat.timer
[Unit]
Description=Ping the heartbeat slot every minute
[Timer]
OnBootSec=30s
OnUnitActiveSec=60s
AccuracySec=5s
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload && sudo systemctl enable --now heartbeat.timer
On a NAS or an appliance without a shell for systemd, the vendor's task scheduler running the same
curl line every minute does the same job. Where only
cron exists, one line: * * * * * curl -fsS --max-time 10 "$HEARTBEAT_URL" >/dev/null 2>&1,
with the variable set at the top of the crontab.
2. macOS: a launchd agent
A per-user agent that launchd runs every 60 seconds and again on login. Save it as
~/Library/LaunchAgents/com.freshjots.heartbeat.plist;
the token sits in the plist (replace YOUR_TOKEN; angle
brackets would break the XML), so make the file readable by you alone
(chmod 600). A sleeping laptop stops pinging and gets reported; that is correct
for a machine that should be awake, and wrong for one that should not, so for "did my long job on this
laptop finish" chain the ping after the job instead:
./job.sh && curl -fsS "$HEARTBEAT_URL".
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.freshjots.heartbeat</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/curl</string>
<string>-fsS</string>
<string>--max-time</string><string>10</string>
<string>https://freshjots.com/hb/YOUR_TOKEN</string>
</array>
<key>StartInterval</key><integer>60</integer>
<key>RunAtLoad</key><true/>
</dict>
</plist>
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.freshjots.heartbeat.plist
launchctl load <path> is the older form of the same
command and still works. To stop the agent for good, remove it with
launchctl bootout gui/$(id -u)/com.freshjots.heartbeat
and switch the slot off.
3. Windows
Task Scheduler running a one-line PowerShell Invoke-WebRequest
every minute, with the URL in a machine-level environment variable. The PowerShell lines that register it are
on the main examples page.
4. Microcontrollers: an ESP32 in the greenhouse
A board with Wi-Fi can be watched the same way, and for most of them this is the only monitoring they will
ever get. The ping is one HTTPS GET a minute from
the main loop. The URL is the credential, so verify the server's certificate with its root CA rather than
calling setInsecure(), which lets anyone on the path
read the token; setInsecure() is fine on the bench,
not in the field.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
const char* HEARTBEAT_URL = "https://freshjots.com/hb/<token>";
const char* ROOT_CA = "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n";
void setup() {
WiFi.begin("your-ssid", "your-password");
while (WiFi.status() != WL_CONNECTED) delay(500);
}
void loop() {
if (WiFi.status() == WL_CONNECTED) {
WiFiClientSecure client;
client.setCACert(ROOT_CA);
HTTPClient http;
if (http.begin(client, HEARTBEAT_URL)) {
http.GET(); // 200 is all we need; a 404 means the slot is off or rotated
http.end();
}
}
delay(60 * 1000);
}
A board that deep-sleeps for longer than two minutes between wakes cannot hold a heartbeat; it belongs on the dead-man's switch, which watches an append-only log with a deadline measured in hours.
5. A client's rack: one slot per site
For every machine you look after on someone else's premises, one slot with the client's name as its label.
The Heartbeats page becomes the list of sites and their state; the Settings → Uptime page gives
each a 7- or 30-day score; and when a client says the box was down all week, the
DAY /
OFF /
ON record, hashed and anchored like every other
Fresh Jots note, is the answer, down to the minute. Point the slot's alert email at the client if they want
to know first, or its webhook at your own on-call channel.
6. Gotchas
- The clock on the box does not matter. Every line in the record is stamped by Fresh Jots in UTC when the check runs; a Pi with a wrong clock produces the same record.
- One household, one IP. Every device behind the same router shares the 60-hits-a-minute IP limit. Once a minute per device leaves room for fifty of them.
- A proxy in the way. On a corporate network that forces an HTTP proxy,
set
https_proxyin the same env file;curlhonours it. - Keep the URL off the device's shared surfaces. Not in a README on the box, not in a wiki page, not in a group chat. If it does leak, Rotate URL on the slot page kills the old one at once and you update the one file on the device.
- Link down and box down look the same from here, by design. If you need to tell them apart, a second slot pinged from another machine on the same link does it: both silent means the link, one silent means the box.