Cron job monitoring with heartbeat pings
Cron ignores exit codes and often discards output. Add curl heartbeats, /start and /fail signals and a reusable wrapper script.
Cron is reliable at one thing: starting a command at a given time. It doesn't check whether the command succeeded, and it doesn't notice when it stops being started. A backup script that has exited with status 1 every night for a month looks the same to cron as one that worked. So does a crontab that was wiped when the server was rebuilt.
This guide shows how to make cron jobs report in, so you're alerted when a job fails, hangs or stops running.
What cron does with failures
- Exit codes are ignored. Cron starts the command and moves on. A non-zero exit status isn't logged as a failure or retried.
- Output goes to mail, if mail works. Anything the job prints to stdout or stderr is mailed to the crontab's owner, or to the address in
MAILTO. On many servers and almost all containers there's no mail transport configured, so that output goes nowhere.MAILTO=""turns it off explicitly. - The environment is minimal. Cron doesn't load your shell profile.
PATHis short, environment variables you rely on interactively are missing, and the working directory is usually the home directory. "Works in my terminal, fails in cron" is almost always this. %is special. In a crontab line, an unescaped%becomes a newline. Everything after the first one is passed to the command as stdin.date +%Fin a crontab must be writtendate +\%F.- The syslog only shows the start. Lines like
CRON[1234]: (root) CMD (/opt/backup.sh)prove the job was launched, not that it finished.
And the failures that don't involve the job at all: the server was replaced, the cron daemon isn't running in the new container image, the crontab was overwritten by a deploy, or the clock or timezone changed.
The heartbeat approach
Instead of watching for errors, expect a signal. When the job finishes successfully it makes an HTTP request to a unique URL. A monitoring service knows how often that should happen. If the signal is late, it alerts you. This catches every failure above, including the ones where nothing runs at all, because they all produce the same symptom: silence.
Step 1: create a monitor
In Flowhush, click New monitor:
- Name:
db-backup on web-1 - Expected every (seconds): the job's interval. Daily is
86400, hourly is3600, every 5 minutes is300. - Grace period (seconds): how late it may be. If you also use
/start(below), the grace period is also the maximum run time, so make it longer than your slowest normal run.
You'll get a URL like https://flowhush.com/p/YOUR-MONITOR-UUID.
Step 2: ping on success
The simplest version adds a curl call after the command, joined with && so it only runs when the command exits with 0:
# crontab -e
0 3 * * * /opt/backup/run.sh && curl -fsS -m 10 --retry 3 -o /dev/null https://flowhush.com/p/YOUR-MONITOR-UUID
What the flags do:
-f: treat HTTP errors as failures (non-zero exit)-sS: no progress bar, but still print errors-m 10: give up after 10 seconds, so a network problem can't hang your cron slot--retry 3: retry transient failures-o /dev/null: discard theOKresponse body
If the backup fails, there's no ping. The monitor goes down after period + grace and you get one alert. When a later run succeeds, you get a recovery alert.
Step 3: report failures and hangs explicitly
Waiting a full day plus grace to learn a daily job failed is slow. Two more endpoints help:
/startat the beginning. If no success ping follows within the grace period, the monitor goes down with "Run started but never reported completion". This catches hung jobs. Flowhush also records the run duration on the completion ping./failon error. The monitor goes down immediately.
0 3 * * * curl -fsS -m 10 -o /dev/null https://flowhush.com/p/YOUR-MONITOR-UUID/start ; /opt/backup/run.sh && curl -fsS -m 10 --retry 3 -o /dev/null https://flowhush.com/p/YOUR-MONITOR-UUID || curl -fsS -m 10 --retry 3 -o /dev/null https://flowhush.com/p/YOUR-MONITOR-UUID/fail
That works, but it's hard to read, and the || also fires if the success ping itself fails. A small wrapper script is cleaner.
Step 4: a reusable wrapper script
Save this as /usr/local/bin/monitored and run chmod +x on it:
#!/usr/bin/env bash
# Usage: monitored <ping-url> <command> [args...]
# Reports start, success or failure (with the exit code and output tail) to Flowhush.
set -uo pipefail
ping_url="$1"; shift
curl -fsS -m 10 --retry 3 -o /dev/null "$ping_url/start" || true
output="$("$@" 2>&1)"
code=$?
printf '%s\n' "$output" # keep the job's output visible to cron mail / logs
if [ "$code" -eq 0 ]; then
curl -fsS -m 10 --retry 3 -o /dev/null "$ping_url" || true
else
{ echo "exit code $code"; printf '%s' "$output" | tail -c 900; } |
curl -fsS -m 10 --retry 3 -o /dev/null --data-binary @- "$ping_url/fail" || true
fi
exit "$code"
Then in the crontab:
PATH=/usr/local/bin:/usr/bin:/bin
0 3 * * * monitored https://flowhush.com/p/YOUR-MONITOR-UUID /opt/backup/run.sh
*/15 * * * * monitored https://flowhush.com/p/OTHER-MONITOR-UUID /opt/sync/pull-orders.py
On failure, the exit code and the last ~900 bytes of output are posted with the /fail ping. Flowhush stores the first 1 KB of each ping body, so you can read them in the monitor's Recent pings without logging into the server. Every curl call ends with || true, so a monitoring outage never changes your job's exit status.
Pipelines and exit codes
A shell pipeline's exit status is the status of its last command. pg_dump mydb | gzip > backup.sql.gz exits 0 even if pg_dump failed, because gzip succeeded. Inside your scripts, use set -o pipefail (bash) so a failure anywhere in the pipeline fails the whole thing. The wrapper above sets it too. Also check that your script actually exits non-zero on errors; set -e helps, with its well-known caveats.
Choosing period and grace
- Flowhush measures the period from the last successful ping. It's an interval, not a cron expression. A job at
0 3 * * *gets a period of86400. - Jobs that only run on weekdays have a weekend gap of about 72 hours. Set the period to cover it, or pause the monitor.
- Grace must exceed your longest normal run time when you use
/start. The minimum for both period and grace is 60 seconds. - The checker runs every minute, so alerts arrive within about a minute of the deadline passing.
Testing without waiting a day
# simulate success, then failure, from any machine
curl -fsS https://flowhush.com/p/YOUR-MONITOR-UUID
curl -fsS --data-binary 'exit code 1: disk full' https://flowhush.com/p/YOUR-MONITOR-UUID/fail
The second command should produce a down alert on your channel within seconds. The next success ping sends the recovery. Use Send test alert on the monitor page to check delivery without changing the monitor's status.
Moving a cron job into n8n
Some teams replace crontab entries with n8n workflows that start with a Schedule Trigger node, so the job is visible and editable in one place. Monitoring then moves into the workflow: add an HTTP Request node as the last step:
Method: POST
URL: https://flowhush.com/p/YOUR-MONITOR-UUID
Send Body: on
Body Content Type: JSON
Specify Body: Using JSON
JSON: {{ JSON.stringify($json) }}
Settings → Execute Once: on
Settings → Retry On Fail: on
Settings → On Error: Continue
Execute Once sends one ping per run instead of one per item. On Error: Continue keeps a monitoring hiccup from failing the job. A self-hosted n8n instance that goes down takes its schedules with it, and the heartbeat is how you find out.
Same idea, other schedulers
The pattern works anywhere you can run curl: systemd timers (ExecStartPost= or a wrapper in ExecStart=), Kubernetes CronJobs (wrap the container command), GitHub Actions schedules (a final step with if: success() and one with if: failure()), and Windows Task Scheduler with curl.exe.
The Flowhush Free plan monitors 5 jobs with email, Slack, Discord or Telegram alerts. Start with the jobs whose failure you'd discover last, such as backups.
Monitor your first workflows free
The Free plan includes 5 heartbeat monitors with alerts to email, Slack, Discord or Telegram. No credit card required.
Start free — 5 monitors See setup snippetsSources
- crontab(5) manual page (MAILTO, % handling, environment)
- curl manual (--fail, --max-time, --retry)
- GNU Bash manual: Pipelines and pipefail
Third-party features and prices were checked on 3 November 2026 and may have changed since.