247Monitor

How to monitor cron jobs and scheduled tasks

A backup that never ran doesn't throw an error — it's just not there the morning you need it. Cron jobs, timers and scheduled tasks fail in silence, which is exactly why an ordinary uptime check can't see them. Here's how to monitor cron jobs the only way that actually works: have them check in, and alert on the run that goes missing.

The 247Monitor Team

We run the monitors so we write about them.

10 min read

Most monitoring watches things that are supposed to answer when you knock. A cron job is the opposite problem: it's supposed to do something and then go quiet. When it doesn't run at all, there's no error page, no 500, nothing to poll — just an absence you won't notice until the day the missing backup or unsent invoice matters. This guide covers how to monitor cron jobs and scheduled tasks properly, with copy-paste recipes for crontab, systemd, Kubernetes and CI.

The short version

  • You can't poll a scheduled job from outside — it has no endpoint. The job has to report in instead.
  • A heartbeat (or “dead man's switch”) monitor gives the job a unique URL to ping each run. Miss a ping and it alerts.
  • Set an expected interval to match the schedule and a grace period for runtime, retries and clock skew.
  • Ping only on success — chain the request with && so a failed run stays silent and trips the alarm.
  • One heartbeat per job, routed to where your team already gets paged, with a recovery notice when it's back.

Why cron jobs fail silently

A broken web page is loud. It throws a status code, renders a stack trace, lights up your error tracker. A scheduled job that simply doesn't run produces none of that — its failure mode is nothing happening, and nothing is remarkably hard to alert on.

The ways a job quietly stops are mundane and numerous:

  • The box rebooted, the container didn't reschedule, or the node it ran on was drained.
  • A deploy rewrote the crontab, dropped the entry, or changed a path so the command isn't found.
  • The job hit a full disk, an expired credential or a missing dependency and died before it logged a thing.
  • cron itself isn't running, or PATH in cron's minimal environment isn't what your shell has.

Each of these leaves no trace in the place you'd look. The backup job that's been failing for three weeks looks identical to one that was never scheduled: an empty bucket. You find out when you reach for the thing it was supposed to produce — which is the worst possible moment to find out.

Why the usual uptime check can't see them

Standard uptime monitoring is a pull model: a prober somewhere on the internet requests your URL every minute and checks the response. That works beautifully for anything with an address — a site, an API, a port. A cron job has no address. There is nothing to request, no door to knock on. Polling cannot help you here.

So the model has to invert. Instead of a monitor reaching in to check the job, the job reaches out to tell the monitor it ran. That single flip is the whole idea:

external check · pull

247Monitorcron job

no URL to hit

A scheduled job has no inbound endpoint. An HTTP check has nothing to probe, so the job is invisible from the outside.

blind to silent failures

heartbeat · push

cron job247Monitor

pings on success

The job checks in each time it runs. Miss a check-in and we alert — the absence is the signal.

catches the run that never happened
You can't poll a cron job from outside — you let it report in instead.

Definition

Heartbeat monitoring (a “dead man’s switch”) is monitoring run in reverse: rather than checking that a service responds, it checks that an expected check-in keeps arriving on schedule. The job pings a unique URL every time it runs, and the absence of that ping past a deadline is what raises the alarm.

How heartbeat monitoring works

When you create a heartbeat monitor you get a unique ping URL and you tell it two things: how often the job should check in (the expected interval) and how long past that you're willing to wait before it counts as missing (the grace period). Every ping resets the clock. Let the clock run past interval + grace with no ping and the monitor opens an incident and alerts you.

heartbeat · nightly-backup · every 24h

ping receivedmissed runalert sent after grace
A job that should run every 24h. Miss a check-in past the grace window and the alert fires — the silence is the signal.

The ping itself is the simplest HTTP request there is — a GET, POST or HEAD to the URL, no body or auth required. A one-line curl on the end of your job does it. Pings are de-duplicated and rate-limited server-side, so it's safe to call on every run, including retried ones. When a run goes missing, the monitor's reasoning is exactly what you'd check by hand:

heartbeat · nightly-backup · push

  • Last pingWed 03:01:12 UTC
  • Expected every24h
  • Grace period5m
  • Time nowThu 03:06:40 UTC
  • Since last ping24h 5m 28s
VERDICT: DOWN — a ping was due by 03:06:12 (24h interval plus 5m grace). None arrived, so 247Monitor opened an incident and alerted your channels.
How a heartbeat decides: expected interval plus grace, then it pages you.
NoteBecause a heartbeat alerts on silence, you want the job to ping only when it actually succeeds. Chain the request after your command with && — if the job exits non-zero, the ping never fires, the deadline passes, and you get paged. That's the behaviour you want.

Set up a heartbeat monitor, step by step

  1. 1

    Create a Cron / Heartbeat monitor

    In the new-monitor dialog, choose the Cron / Heartbeat type and give it a name you'll recognise in an alert — nightly-backup, not monitor 7. There's nothing to point it at; on save you get a unique ping URL.

  2. 2

    Set the expected interval

    Match it to the schedule the job runs on. An hourly job expects a ping every 1 hour; a nightly backup, every 24 hours. This is the cadence the monitor measures silence against.

  3. 3

    Set a grace period

    The grace period is headroom — time for the job to actually run, for a retry, and for a little clock drift — before a late ping counts as a missed one. The default is 60 seconds; a long-running export deserves several minutes.

  4. 4

    Add the ping to your job

    Append a curl to the end of the command so it fires when — and only when — the work succeeds. The recipes below cover crontab, systemd, Kubernetes and CI.

  5. 5

    Route the alert (and a recovery notice)

    Send the alert where your team already looks — Slack, Teams, Telegram, Discord, email or SMS — and enable the recovery notification so a resumed job tells you it's caught up, not just that it broke.

  6. 6

    Test it by forcing a miss

    Don't trust a monitor you've never seen fire. Comment out the job for one cycle, or temporarily set a tiny interval, and confirm the alert lands in the right channel before you move on.

Recipes for every scheduler

The pattern is always the same: run the job, and on success, ping the URL. The flags matter — -f fails on HTTP errors, -s -S stays quiet but still surfaces real problems, -m 10 caps the request at ten seconds so a slow monitor can never hang your job, and --retry 3 rides out a network blip. Swap the token below for your own.

Classic crontab

# Nightly backup at 03:00 — ping only if the job succeeds
0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 \
  https://app.247monitor.net/api/v1/heartbeat/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6

systemd timer

For a oneshot service driven by a .timer, ExecStartPost runs only if the main command succeeded — the same guarantee && gives you in a shell.

# /etc/systemd/system/backup.service  (paired with backup.timer)
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# ExecStartPost only runs if ExecStart exited 0:
ExecStartPost=/usr/bin/curl -fsS -m 10 \
  https://app.247monitor.net/api/v1/heartbeat/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6

Kubernetes CronJob

A CronJob can be scheduled correctly and still never do its work — an ImagePullBackOff, an evicted pod, a bad config map. Chain the ping onto the container command so it only fires on a clean exit.

# Kubernetes CronJob — chain the ping onto the container command
spec:
  schedule: "0 3 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: backup
              image: example/backup:latest
              command: ["/bin/sh", "-c"]
              args:
                - backup.sh && curl -fsS -m 10
                  https://app.247monitor.net/api/v1/heartbeat/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6

Scheduled CI (GitHub Actions, GitLab)

Scheduled workflows skip silently when a runner is busy or a cron trigger is paused on a fork. A final step that runs if: success() turns that silence into an alert.

# .github/workflows/backup.yml
on:
  schedule:
    - cron: "0 3 * * *"
jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - run: ./backup.sh
      - name: Heartbeat
        if: success()   # don't check in if the job failed
        run: curl -fsS -m 10 https://app.247monitor.net/api/v1/heartbeat/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
TipOn Windows, end a Task Scheduler action with PowerShell — Invoke-RestMethod -Uri "https://app.247monitor.net/api/v1/heartbeat/…" -TimeoutSec 10 — and for in-app schedulers (a Node cron, a Celery beat task) just fetch() or requests.get() the URL at the end of the job. If the URL lives in a public repo or CI config, store it as a secret — treat the token like a password.

Choosing the interval and grace period

These two numbers decide whether your heartbeat is useful or just annoying. The interval should mirror the schedule exactly. The grace period is the judgement call: too tight and a job that's merely slow pages you at 3am; too loose and a genuinely dead job goes unnoticed for hours.

  • A good default for grace is worst-case runtime + one retry + a margin. If a backup usually takes 90 seconds but occasionally 4 minutes, don't set grace to 60 seconds.
  • For jobs with wild runtime variance, lean towards a generous grace and accept slightly slower detection — a false alarm every week is how a channel gets muted.
  • If a job runs many times an hour, a single missed run may be normal; widen the interval so you alert on a sustained gap, not one blip.

Mistakes to avoid

  • Pinging unconditionally. Using ; instead of && means a failed job still checks in — the monitor stays green while the work is broken. Ping on success only.
  • Grace set too tight. The number-one cause of heartbeat false alarms. Measure your real runtime before you set it.
  • Watching the host, not the job. A server can be perfectly up while the cron daemon is dead or the entry is gone. The heartbeat proves the job ran, which is the thing you care about.
  • One heartbeat for many jobs. Share a URL across your backup, ETL and report jobs and a missed ping tells you something broke but not what. Use one per job.
  • No recovery notification. Knowing a job failed is half the story; knowing it recovered stops a needless 6am drive to the server room.

Frequently asked questions

How do I monitor a cron job?

Create a heartbeat (dead man's switch) monitor, which gives you a unique ping URL. Set the interval you expect the job to run on and a grace period, then have the job send an HTTP request to that URL when it finishes successfully. If a ping doesn't arrive in time, you get alerted — the missing run is the signal.

What is a heartbeat or dead man's switch monitor?

It is monitoring inverted. Instead of checking that something responds when you poll it, a heartbeat checks that an expected signal keeps arriving on schedule. Silence past the deadline is the failure. That's the only model that works for jobs which have no endpoint to probe.

Can I monitor Kubernetes CronJobs, systemd timers and CI schedules?

Yes. Anything that can make an HTTP request can send a heartbeat. Append a curl to your crontab line or container command, add an ExecStartPost to a systemd service, or add a final step to a scheduled GitHub Actions or GitLab CI job that runs only on success.

What grace period should I set for a heartbeat?

Enough to cover the job's worst-case runtime plus one retry and a little clock skew — not so tight that a merely slow run pages you. The 60-second default suits quick tasks; give a multi-minute backup or ETL job several minutes of grace.

Can I monitor cron jobs for free?

Yes. 247Monitor's free plan includes 25 monitors — heartbeats among them — with every alert channel and no card required. That's enough to watch your backups, certificate renewals, queue workers and nightly reports.

The bottom line: the jobs most likely to ruin your week are the ones that fail without a sound — the backup, the renewal, the nightly export. A heartbeat turns their silence into a signal, so you hear about the run that didn't happen before you need what it was supposed to produce. 247Monitor includes heartbeat checks on every plan — the free tier covers 25 monitors with no card, and you can create them over the API too. Start watching your cron jobs free →

No credit card · 25 monitors free · 2-minute setup

Start monitoring in minutes.

Free forever for 25 monitors and a server. Add your first check, pick your alert channels, and you're live before your coffee's cold.