March 2026
The pattern is simple: after your job runs, send a ping to a monitoring service. If the ping doesn't arrive on time, you get an alert. That's it.
This guide shows you how to do that for every common scheduler — crontab, systemd timers, Docker, Kubernetes CronJobs, and CI/CD pipelines. The examples use CronPulse, but the pattern works with any heartbeat monitoring tool.
Every heartbeat monitor works the same way:
curl to the end of your jobThe ping URL looks something like this:
https://ping.trebben.dk/a1b2c3d4e5f6
The most common case. You have a job in crontab -e and you want to know if it stops running.
# Backup runs at 2am, ping on success
0 2 * * * /usr/local/bin/backup.sh && curl -fsS https://ping.trebben.dk/YOUR_SLUG
The && means the ping only fires if the backup exits 0. If the script fails, no ping — and you get alerted.
# Run job, capture exit code, always ping
0 2 * * * /usr/local/bin/backup.sh; curl -fsS https://ping.trebben.dk/YOUR_SLUG/$?
Using ; instead of && means the ping fires either way. Appending $? sends the exit code — your monitoring tool can alert on non-zero exits while still knowing the job ran.
# Kill the job if it hangs for more than 1 hour
0 2 * * * timeout 3600 /usr/local/bin/backup.sh && curl -fsS https://ping.trebben.dk/YOUR_SLUG
A cron job that hangs indefinitely won't trigger a "missed" alert because it's still running. timeout prevents this.
# -f: fail silently on HTTP errors
# -s: silent mode (no progress bar)
# -S: show errors even in silent mode
# --max-time 10: don't let curl hang
curl -fsS --max-time 10 https://ping.trebben.dk/YOUR_SLUG
--max-time with curl in cron. If the monitoring service is briefly unreachable, you don't want curl blocking your next job.
If you use systemd timers instead of cron, add the ping as an ExecStartPost directive.
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
ExecStartPost=/usr/bin/curl -fsS --max-time 10 https://ping.trebben.dk/YOUR_SLUG
ExecStartPost only runs if ExecStart succeeds. If you want to ping regardless:
# The - prefix means "don't fail the unit if this command fails"
ExecStartPost=-/usr/bin/curl -fsS --max-time 10 https://ping.trebben.dk/YOUR_SLUG
#!/bin/sh
# entrypoint.sh
/app/process-queue.sh
curl -fsS --max-time 10 https://ping.trebben.dk/YOUR_SLUG
# docker-compose.yml
services:
backup:
image: your-backup-image
command: >
sh -c '/app/backup.sh &&
curl -fsS --max-time 10 https://ping.trebben.dk/YOUR_SLUG'
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-backup
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: your-backup-image
command:
- sh
- -c
- |
/app/backup.sh &&
curl -fsS --max-time 10 https://ping.trebben.dk/YOUR_SLUG
restartPolicy: OnFailure
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh
- run: curl -fsS --max-time 10 https://ping.trebben.dk/YOUR_SLUG
name: Report success to CronPulse
deploy:
script:
- ./deploy.sh
after_script:
- curl -fsS --max-time 10 https://ping.trebben.dk/YOUR_SLUG
If you want to wrap any script with monitoring without modifying the script itself:
#!/bin/sh
# monitor-wrap.sh — wraps any command with heartbeat monitoring
# Usage: monitor-wrap.sh YOUR_SLUG command [args...]
SLUG="$1"
shift
"$@"
EXIT_CODE=$?
curl -fsS --max-time 10 "https://ping.trebben.dk/${SLUG}/${EXIT_CODE}" || true
exit $EXIT_CODE
Then in your crontab:
0 2 * * * /usr/local/bin/monitor-wrap.sh a1b2c3 /usr/local/bin/backup.sh
Set the monitor's expected period to match how often your job runs. Add a grace period for execution time:
The grace period accounts for the time the job itself takes to run. A backup that takes 20 minutes needs at least 20 minutes of grace.
--max-time — if the monitoring service is down, curl hangs forever and your next job can't start&& when you meant ; — with &&, a failed job means no ping, which means an alert for "missed schedule" when the real problem was "job failed." Both are worth knowing, but they're different signalsmysqldump tells you the dump ran, not that it contains data. Add a sanity check: test -s /backup/dump.sql && curl ...If you want to try this with CronPulse:
The free tier is enough for most setups. If you need more than 20 monitors, the pro plan is $5/month.
The full API docs are at cronpulse.trebben.dk/docs.