Back to Blog
Technical 9 min read

Cron Job Monitoring: How to Know When a Scheduled Job Silently Fails

AlertSleep Team
AlertSleep Team
Monitoring & DevOps
October 14, 2026

Short answer: cron runs your job and tells nobody what happened. If the job fails, hangs or never starts, you find out when a customer, an empty backup folder or a missing invoice tells you. Cron job monitoring turns that around: the job calls a URL when it finishes, and if the call does not arrive on schedule, you get an alert. This is called heartbeat monitoring, and it takes one line in your crontab.

Facts verified on 14 October 2026 against the crontab manual page, the Laravel scheduling documentation and GitHub's documentation for scheduled workflows. Sources are listed at the end.

Why cron jobs fail silently

A web page that stops working gets noticed within minutes. A nightly job that stops working gets noticed when someone needs what it was supposed to produce. The usual causes:

  1. A different environment. According to the crontab manual, cron sets only SHELL=/bin/sh, LOGNAME and HOME for your job. Everything your login shell loads β€” extra PATH entries, version managers, exported variables β€” is missing. A script that works when you run it by hand fails under cron.
  2. A percent sign. In a crontab line, an unescaped % becomes a newline and everything after it is sent to the command as input. date +%F in a backup file name quietly breaks the whole line.
  3. Output that goes nowhere. Cron mails a job's output to the crontab owner, or to MAILTO. On most servers that mailbox is never read, or no mail is delivered at all β€” so the error message exists, but nobody sees it.
  4. A job that never finishes. A network call without a timeout or a lock left behind by a crashed run, and the job hangs. No error, no output, just nothing.
  5. Daylight saving time. The crontab manual says it plainly: times that do not exist during the change never match, so those jobs do not run, and times that occur twice make jobs run twice.
  6. The schedule disappears. A server migration, a rebuilt container or a new deploy without the crontab β€” and the job simply stops existing.
  7. The platform scheduler is not a guarantee. GitHub's documentation says scheduled workflows can be delayed during periods of high load, that some queued jobs may be dropped, and that in a public repository they are disabled automatically after 60 days without repository activity.

None of these produce an alert on their own. That is the real problem with cron: failure looks exactly like success β€” silence.

Why uptime monitoring does not catch it

Uptime monitoring asks your server a question and checks the answer. A cron job does not answer questions: there is no port to connect to and no page to load. The failure you need to detect is an absence β€” a run that should have happened and did not. Only something that expects a signal at a given time can notice that the signal never came.

How heartbeat monitoring works

You tell the monitoring service when the job should run β€” every 15 minutes, or a cron expression such as 0 3 * * * β€” and how much delay is acceptable (the grace period). The job pings a unique URL every time it finishes successfully. The service then knows which state the job is in:

SituationState in the dashboardAlert
The ping arrived on scheduleHealthyβ€”
The scheduled time has passed, still within the grace periodLateβ€” (normal variation)
No ping by the scheduled time plus the grace periodMissedYes
The job reported a failureMissedYes, immediately
The job reported a start and never finishedMissedYes, after the grace period
The next successful ping arrivesHealthyRecovery notice

The last two rows are why a good setup sends more than one signal. A ping at the end tells you the job finished. A ping at the start as well tells you it began β€” so a job that hangs halfway is caught instead of looking like one that never ran. And an explicit failure ping raises the alarm straight away instead of waiting for the deadline.

Add monitoring to a cron job

The simplest version pings only after a successful run. && matters: if the job fails, the ping is never sent and the check goes down at the deadline.

0 3 * * * /path/to/backup.sh && curl -fsS --retry 3 https://api.alertsleep.com/v1/ping/YOUR-TOKEN

The full version reports the start, the success and the failure:

0 3 * * * curl -fsS --retry 3 https://api.alertsleep.com/v1/ping/YOUR-TOKEN/start; if /path/to/backup.sh; then curl -fsS --retry 3 https://api.alertsleep.com/v1/ping/YOUR-TOKEN; else curl -fsS --retry 3 https://api.alertsleep.com/v1/ping/YOUR-TOKEN/fail; fi

Two details are deliberate. The start ping is followed by ;, not &&, so the job still runs if the ping cannot be delivered. And the line uses if/then/else rather than the popular job && success || fail shorthand, which reports a failure when the job succeeded but only the success ping failed to arrive.

You can also pass the exit code and let the monitoring service decide. A non-zero code sent to the success URL is recorded as a failure:

0 3 * * * /path/to/backup.sh; curl -fsS --retry 3 --data "exit_code=$?" https://api.alertsleep.com/v1/ping/YOUR-TOKEN

The curl options are worth keeping: -f makes an HTTP error count as an error, -sS stays quiet unless something goes wrong, and --retry 3 survives a brief network hiccup.

Laravel scheduler

If the job is already a scheduled Laravel command, the scheduler can call the URLs itself β€” no shell quoting and no exit codes to forward by hand:

// routes/console.php
Schedule::command('backup:run')
    ->cron('0 3 * * *')
    ->pingBefore('https://api.alertsleep.com/v1/ping/YOUR-TOKEN/start')
    ->pingOnSuccess('https://api.alertsleep.com/v1/ping/YOUR-TOKEN')
    ->pingOnFailure('https://api.alertsleep.com/v1/ping/YOUR-TOKEN/fail');

One more thing to watch in Laravel: the whole scheduler depends on a single crontab entry running php artisan schedule:run every minute. If that entry is lost, every scheduled task stops at once β€” which is exactly what a heartbeat on your most important task will tell you.

GitHub Actions scheduled workflows

Because scheduled workflows can be delayed, dropped or disabled, they need a heartbeat as much as a server does. Keep the ping URL in a repository secret, treat it like a password, and add three steps:

on:
  schedule:
    - cron: '0 3 * * *'

jobs:
  nightly:
    runs-on: ubuntu-latest
    steps:
      - run: curl -fsS --retry 3 "${{ secrets.ALERTSLEEP_PING_URL }}/start" || true
      - run: ./scripts/nightly.sh
      - if: success()
        run: curl -fsS --retry 3 "${{ secrets.ALERTSLEEP_PING_URL }}"
      - if: failure()
        run: curl -fsS --retry 3 "${{ secrets.ALERTSLEEP_PING_URL }}/fail"

|| true on the start ping keeps the workflow running if the ping cannot be delivered. A run that GitHub delays past your grace period, or never starts at all, shows up as a missed heartbeat β€” the one failure the workflow itself can never report.

How long should the grace period be?

Long enough for the job's normal variation, short enough that a real problem is still fresh. A useful rule: take the longest run you have seen and add a margin. A backup that usually takes 20 to 40 minutes needs about an hour; a cleanup job that takes seconds is fine with five minutes. If the check keeps going late without anything being wrong, the grace period is too short β€” and an alert that is usually a false alarm teaches people to ignore it.

How AlertSleep monitors cron jobs

A cron check in AlertSleep is a schedule and a ping URL:

  • the schedule is either a period β€” from one minute up to 30 days β€” or a cron expression with its own time zone;
  • the grace period can be anything from 30 seconds to 24 hours, five minutes by default;
  • the ping URL accepts GET, POST and HEAD, with /start and /fail variants, and optionally an exit code, a duration and a short message;
  • a job that sent /start and nothing more within the grace period is reported as hung, not as missed;
  • alerts arrive by email and in the dashboard when a check goes down, and a recovery notice follows the next successful ping;
  • every ping is kept with its exit code and duration, so you can see when a job started taking longer;
  • the dashboard shows ready-to-paste crontab and Laravel snippets for each check, and the token can be regenerated if a ping URL leaks.

Details are on the cron monitoring page. If you are still writing the schedule itself, the free Cron Expression Generator builds an expression from a form and lists its next five runs, and the Cron Expression Cheat Sheet covers the patterns you will actually use.

A 20-minute cron checklist

  1. Find every scheduled job. crontab -l for each user, the files in /etc/cron.d, systemctl list-timers, scheduled CI workflows and any scheduler inside your application.
  2. For each one, ask what breaks if it does not run for a week. Backups, invoices, data syncs, certificate renewals and cleanups usually top the list.
  3. Add a heartbeat to the important ones β€” start with backups, because a failed backup is only discovered when you need to restore.
  4. Set grace periods from real run times, not guesses.
  5. Test it: stop one job deliberately, or point it at a wrong path, and check that the alert arrives.
  6. Escape every % in your crontab lines and set the environment the job needs explicitly, at the top of the script.

FAQ

What is the difference between cron monitoring and uptime monitoring?

Uptime monitoring checks something that is always there β€” a website, an API, a port. Cron monitoring waits for a signal from something that runs only now and then, and alerts when the signal does not come.

Does the job need to be reachable from the internet?

No. The job only makes an outgoing HTTPS request to the ping URL. Nothing has to connect to your server.

What happens if the ping cannot be delivered?

--retry 3 handles short network problems. If the ping still does not arrive, the check goes down at the deadline and you get an alert. That can occasionally be a false alarm, but it is the safe direction: a missed real failure costs far more than a needless email.

Can I monitor jobs that run more often than once a minute?

The shortest period is one minute. For jobs that run more often, ping once per minute from the job or monitor the process that starts them.

Is the ping URL a secret?

Yes. Anyone who has it can report your job as healthy. Keep it out of public repositories, store it in a secret in CI, and regenerate the token if it ever leaks.

Sources

#cron job monitoring #heartbeat monitoring #cron job failed alert #monitor scheduled tasks #laravel scheduler monitoring

Share this article

About the Author

AlertSleep Team

AlertSleep Team

Monitoring & DevOps

The AlertSleep team helps developers and ops teams keep their services online and communicate outages professionally.

Related Articles

Stay Updated

Get the latest articles and monitoring tips delivered to your inbox.