Skip to content

Heartbeat examples

Each example runs a job called backup, then calls a Heartbeat Monitor’s URL. Replace both: the job with yours, and https://api.uup.dev/hb/your-token with the Heartbeat URL on your Monitor’s Checks card.

Every example keeps two rules:

  • Call on success only. The call comes after the job, and runs only when the job succeeded. A job that failed stays silent, and the Monitor goes down once its Grace runs out.
  • Wait a short time. Each call gives up after 10 seconds, so a slow answer from us never holds your job. The curl line, the one the Checks card shows, also tries again up to 3 times, so a short break on our side does not fail the call.

Keep the URL secret: anyone who has it can call it. In a repository, keep it in your platform’s secrets rather than in the file.

To choose how often the job must call, and how late it may be, see Heartbeat Monitors.

Add the call to the job’s own line, after &&, so it runs only when the job succeeded:

Terminal window
0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 https://api.uup.dev/hb/your-token

Give the Monitor the same schedule: choose Custom cron, and enter the line’s five fields.

A timer starts a service. Make the call the service’s ExecStartPost=, which runs only once every ExecStart= has succeeded:

/etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# Runs only once ExecStart has succeeded.
ExecStartPost=/usr/bin/curl -fsS -m 10 --retry 3 https://api.uup.dev/hb/your-token
# /etc/systemd/system/backup.timer
[Unit]
Description=Run the nightly backup
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target

Start the timer with systemctl enable --now backup.timer.

A step runs only when every step before it succeeded, so make the call the last step:

.github/workflows/backup.yml
name: Nightly backup
on:
schedule:
- cron: "0 3 * * *"
jobs:
backup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./backup.sh
# Runs only when every step above succeeded.
- run: curl -fsS -m 10 --retry 3 https://api.uup.dev/hb/your-token

In a public repository, store the URL as a secret, and write ${{ secrets.UUP_HEARTBEAT_URL }} in its place. A workflow’s cron is read in UTC. Unless your Project’s timezone is UTC, write the Monitor’s schedule with the same times in the Project’s timezone.

The container runs the job and then the call, in one shell command. Its image needs curl. With restartPolicy: Never, a failed call does not run the job a second time.

apiVersion: batch/v1
kind: CronJob
metadata:
name: backup
spec:
schedule: "0 3 * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: backup
image: registry.example.com/backup:latest
command:
- /bin/sh
- -c
- ./backup.sh && curl -fsS -m 10 --retry 3 https://api.uup.dev/hb/your-token

In a script, join the job and the call with &&:

Terminal window
./backup.sh && curl -fsS -m 10 --retry 3 https://api.uup.dev/hb/your-token

In a script that runs with set -e, the call can simply be its last line: the script stops at the first command that fails.

fetch is built into Node, and AbortSignal.timeout makes it give up. The example uses await outside a function, so save it as a .mjs file:

// Throws when the backup fails, so uup is never called.
await backup();
await fetch("https://api.uup.dev/hb/your-token", { signal: AbortSignal.timeout(10000) });

urllib.request is in the standard library, so nothing needs installing:

import urllib.request
# Raises when the backup fails, so uup is never called.
backup()
urllib.request.urlopen("https://api.uup.dev/hb/your-token", timeout=10)
package main
import (
"log"
"net/http"
"time"
)
func main() {
if err := backup(); err != nil {
log.Fatal(err) // The backup failed, so uup is never called.
}
client := &http.Client{Timeout: 10 * time.Second}
response, err := client.Get("https://api.uup.dev/hb/your-token")
if err != nil {
log.Fatal(err)
}
response.Body.Close()
}

net/http is in the standard library:

require "net/http"
# Raises when the backup fails, so uup is never called.
backup
uri = URI("https://api.uup.dev/hb/your-token")
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
open_timeout: 10, read_timeout: 10) do |http|
http.get(uri.path)
end

file_get_contents calls a URL when allow_url_fopen is on, as it is by default:

<?php
// Throws when the backup fails, so uup is never called.
backup();
file_get_contents("https://api.uup.dev/hb/your-token", false, stream_context_create([
"http" => ["timeout" => 10],
]));