Guides / Monitoring guides

Cron job monitoring with heartbeat pings

Updated · 6 min read

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. PATH is 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 +%F in a crontab must be written date +\%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 is 3600, every 5 minutes is 300.
  • 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 the OK response 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:

  • /start at 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.
  • /fail on 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 of 86400.
  • 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 snippets

Sources

Third-party features and prices were checked on 3 November 2026 and may have changed since.