Switching from UptimeRobot? Import your monitors in one click with a read-only API key.Start free →
  1. Home
  2. Blog
  3. Engineering

Monitor a cron job with one curl command

Cron jobs fail silently. Add one curl line and get an alert when a backup, import or queue worker does not run, runs late or exits with an error. Includes systemd, Docker and Kubernetes.

Engineering

The nightly backup has been failing for three weeks. Nobody knew, because a cron job that fails produces no error page, no red status and no customer complaint. It produces a restore attempt in month four that finds nothing.

The fix is small enough to fit in one line of your crontab. This guide shows the pattern, the variations for the platforms you probably use, and the mistakes that make a heartbeat monitor lie.

The idea: let the job check in

A normal uptime monitor asks your site “are you there?” A scheduled job has nothing to ask. The only signal is that it ran. So you reverse the direction: when the job finishes, it requests a URL. If that request does not arrive on time, the monitor alerts you.

How a heartbeat monitor works The cron job requests a start URL, runs, then requests the success URL or the fail URL. The monitor expects a success ping inside the period plus a grace window. A missing ping, a late ping or a fail ping each raise an alert. Cron job0 3 * * * backup.sh 1. /start (optional) 2. /h/ID when it succeeds or /h/ID/fail when it fails Heartbeat monitorExpects a ping every period(or on a cron schedule) plus a grace window. What the monitor does Ping on timeState: up. No alert. Ping inside the grace windowState: late. Waiting. No ping, or /failState: down. Incident opens,alert goes out.
Silence is the failure signal. That is why it catches jobs that never start.

Step 1: create the monitor

Create a heartbeat monitor. You get a URL of the form https://ping.tenium.app/h/<id>. The ID is the secret, so treat the URL like a password and keep it out of public repositories. Heartbeat monitors start on the Starter plan ($7 a month, 20 heartbeats); Pro includes 100 and Agency 500.

Choose how the schedule is expressed:

  • A period (every 15 minutes) for workers and queue consumers.
  • A cron expression with a time zone (0 3 * * *, Europe/Tallinn) for jobs that run at fixed times. If you are unsure about the expression, the cron expression builder reads it back in plain English and lists the next five runs.

Then set the grace time: the longest normal run plus a margin.

Step 2: add the line to your crontab

The smallest useful version:

0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 https://ping.tenium.app/h/abc123 > /dev/null

Three details in that line matter:

  • && means curl runs only if the script exited with success. With ; you would report success for a failed job.
  • -m 10 --retry 3 stops a slow network from hanging the cron entry and retries a failed ping.
  • -fsS fails on HTTP errors, stays quiet on success and still prints errors, which cron mails to you.

To report failures right away instead of waiting for the grace window to run out:

0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 https://ping.tenium.app/h/abc123 || curl -fsS -m 10 https://ping.tenium.app/h/abc123/fail

Step 3 (better): wrap the job once

When you have ten jobs, copy-pasting that line gets messy. A small wrapper keeps it in one place and sends the exit code and the end of the output, which then appear in the incident:

#!/bin/sh
# usage: ping-wrap ID command args...
ID="$1"; shift
BASE="https://ping.tenium.app/h/$ID"
curl -fsS -m 10 "$BASE/start" > /dev/null
OUT=$("$@" 2>&1); CODE=$?
if [ "$CODE" -eq 0 ]; then
  curl -fsS -m 10 --retry 3 "$BASE" > /dev/null
else
  printf '%s' "$OUT" | tail -c 4000 | curl -fsS -m 10 --data-binary @- "$BASE/fail?exit=$CODE" > /dev/null
fi
exit "$CODE"

Use it as 0 3 * * * /usr/local/bin/ping-wrap abc123 /usr/local/bin/backup.sh. The /start call enables duration tracking: you see how long the job takes, and a job that starts but never finishes shows up as late.

Choosing the period and the grace

The two numbers decide how fast you hear about a problem and how often you are woken by nothing. Start from how the job really behaves, not from how it is meant to behave.

JobScheduleGraceWhy
Nightly database backup0 3 * * *45 minutesRuns 10 to 25 minutes; month-end is slower
Queue worker heartbeatevery 5 minutes5 minutesOne missed ping is a stuck worker, so be strict
Hourly import from a supplier15 * * * *20 minutesThe supplier’s API is sometimes slow
Weekly report e-mail0 7 * * 12 hoursWaits for other jobs to finish first

Look at the last ten runs before you pick a grace: take the slowest, add half of it, and round up. Review it when the job gets slower.

Test it before you trust it

A monitor you have never seen fail is a guess. Test all three outcomes once, on a quiet afternoon:

  1. Run the curl line by hand and check the monitor turns up with a fresh ping time.
  2. Request the /fail URL by hand and confirm that the incident opens and the alert reaches the contact you actually read.
  3. Comment the job out of the crontab, wait for the period plus the grace to pass, and confirm the “no ping” alert arrives. Then put the job back.

Write the monitor’s name and the job’s owner next to the crontab entry. In a year, nobody will remember which of twenty heartbeats belongs to which script.

Variations by platform

systemd timer. Add a line to the service unit:

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
ExecStartPost=/usr/bin/curl -fsS -m 10 https://ping.tenium.app/h/abc123

With Type=oneshot, ExecStartPost runs only after ExecStart finished successfully, so a failed job sends no ping.

Docker. Ping from the same shell command as the job, because a container that exits has no “after”:

docker run --rm myimage sh -c 'run-job && wget -q -O- https://ping.tenium.app/h/abc123'

Kubernetes CronJob. Add the ping as the last step of the container command, or run a second container in the pod that waits for the first. Keep the ID in a Secret, not in the manifest.

Windows Task Scheduler. As a PowerShell action: .\backup.ps1; if ($?) { Invoke-RestMethod https://ping.tenium.app/h/abc123 | Out-Null }.

WordPress. wp-cron runs only when somebody visits. The five WordPress checks post has a ready-made must-use plugin that pings on each scheduled run.

Mistakes that make a heartbeat lie

  1. Pinging at the start only. You learn that the job began, not that it finished. Ping at the end, use /start as an extra.
  2. ; instead of &&. The ping goes out even though the script failed.
  3. Pinging from a different place than the job runs. If the job is on server A and the cron ping is on server B, the monitor says nothing about A.
  4. A grace period shorter than the worst run. Month-end runs are the slowest. Size the grace for them.
  5. Time zone drift. A cron expression in the monitor must use the same zone as the server, including daylight saving changes.
  6. Overlapping runs. A job that takes longer than its interval starts twice. Use flock -n /tmp/backup.lock in front of the script, and let the monitor tell you when runs are skipped.
  7. Success that means nothing. A backup script that exits 0 after writing an empty file is a green light on a broken backup. Make the script check the output size, or run a restore test and ping only if it passes.

What to put in front of it

For every job that matters, the checklist is short: a heartbeat with a sensible grace, an alert contact you read (email on every plan, Telegram on paid plans), and a note of who owns the job. See cron job monitoring for the options, and the Tenium vs Cronitor comparison if cron and queue monitoring is your main requirement: Cronitor is built around exactly that, and the page says where it is stronger.

Create a free account to try the rest of the checks; heartbeats start on Starter.

Frequently asked questions

What is heartbeat monitoring?

Instead of checking your server from outside, you let the job check in. Each time it finishes it requests a unique URL. If the request does not arrive within the expected period plus a grace window, you get an alert. It is the only way to notice a job that never started.

How is a heartbeat different from an uptime check?

An uptime check asks a public URL whether it answers. A cron job has no public URL, and when it stops nothing is down from the outside. A heartbeat flips the direction: silence is the failure.

Which plan do I need for cron monitoring?

Heartbeat monitors start on the Starter plan ($7 a month, 20 heartbeats). Pro includes 100 and Agency 500. The Free plan does not include heartbeats.

What should the grace period be?

Set it to the longest normal run time plus a margin. A nightly backup that takes 20 minutes and runs at 03:00 can use a cron schedule of 0 3 * * * and a grace of 45 minutes. Too short produces false alerts, too long delays real ones.

Can I report a failure, not only silence?

Yes. Request the same URL with /fail appended and the monitor goes down at once and opens an incident. You can add an exit code and the last lines of output, and they appear in the incident.


Related

Related Tenium pages

Monitor your first site free

30 monitors free, checks from 2 regions, no card. SSL and domain expiry from $7 a month.