Back to Blog
Tutorial 8 min read

How to monitor cron jobs in 2026. The complete guide.

Published March 21, 2026

Right now, somewhere in your infrastructure, a cron job is failing silently. Maybe it's a backup that stopped writing to disk. Maybe it's a data sync that's been timing out for a week. Maybe it's a cleanup script that broke after the last deploy.

You don't know. And you won't know — until a customer reports missing data, or a disk fills up, or an invoice doesn't go out.

This guide shows you how to fix that in 30 seconds per job.

Why cron jobs fail silently

The Unix cron daemon has one job: execute a command on a schedule. That's it. It doesn't care if the command succeeds. It doesn't track if the command finished. It doesn't alert anyone if the command never runs at all.

Ways cron jobs fail that you won't catch without monitoring:

Server reboots and cron service doesn't restart
Script exits early due to an unhandled error
Disk full — backup runs but can't write
API rate limit hit — data sync silently aborts
Permission change breaks the script after a deploy
Environment variable missing in cron's limited context
Dependency updated and broke compatibility
Network timeout kills the connection mid-job

Every one of these scenarios results in the same thing: silence. No error in your inbox. No alert in Slack. Just a job that stopped working, and nobody who knows about it.

How heartbeat monitoring works

Heartbeat monitoring (also called dead man's switch monitoring) flips the approach. Instead of checking if a job is running, you check if a job stops running.

1
You create a monitor with an expected schedule (every 5 minutes, every hour, daily)
2
Your cron job pings a unique URL every time it completes successfully
3
The monitoring service expects that ping at regular intervals
4
If a ping doesn't arrive on time → you get an alert immediately

This catches every type of failure — script errors, server crashes, cron daemon issues, network problems. If the ping doesn't arrive, you know something is wrong. The reason doesn't matter. The alert does.

Set it up in 30 seconds

We'll use PingCron for these examples. The concept is the same for any heartbeat monitoring tool — but PingCron is free to start and takes 30 seconds to set up.

01

Sign up at pingcron.io

Free. No credit card. 10 seconds.

02

Create a monitor

Name it, set the schedule, set a grace period. You'll get a unique ping URL.

03

Add one line to your cron job

Paste the curl command at the end of your script. Done.

Code examples for every language

Every example below uses the same ping URL from PingCron. The pattern is identical: run your job, then ping. If the job fails, ping the /fail endpoint instead.

Bash (most common)

crontab -e
# Basic: ping after success
0 2 * * * /scripts/backup.sh \
  && curl -s https://api.pingcron.io/ping/abc123

# Better: report success or failure
0 2 * * * /scripts/backup.sh \
  && curl -s https://api.pingcron.io/ping/abc123 \
  || curl -s https://api.pingcron.io/ping/abc123/fail

# Best: report start, success, and failure
0 2 * * * curl -s https://api.pingcron.io/ping/abc123/start \
  && /scripts/backup.sh \
  && curl -s https://api.pingcron.io/ping/abc123 \
  || curl -s https://api.pingcron.io/ping/abc123/fail

The "best" version lets PingCron track job duration — you'll see how long each run takes in your dashboard.

Want to try this right now?

Create a free monitor and get your ping URL in 30 seconds.

Create free monitor

Python

backup.py
import requests

PING = "https://api.pingcron.io/ping/abc123"

def run():
    requests.get(f"{PING}/start")
    try:
        run_backup()
        requests.get(PING)       # success
    except Exception:
        requests.get(f"{PING}/fail")
        raise

if __name__ == "__main__":
    run()

Node.js

job.js
const PING = "https://api.pingcron.io/ping/abc123";

async function runJob() {
  await fetch(`${PING}/start`);
  try {
    await processData();
    await fetch(PING);           // success
  } catch (err) {
    await fetch(`${PING}/fail`);
    throw err;
  }
}

runJob().catch(console.error);

PHP

task.php
<?php
$ping = "https://api.pingcron.io/ping/abc123";

file_get_contents("$ping/start");
try {
    runMigration();
    file_get_contents($ping);    // success
} catch (Exception $e) {
    file_get_contents("$ping/fail");
    throw $e;
}

Go

main.go
package main

import "net/http"

const ping = "https://api.pingcron.io/ping/abc123"

func main() {
    http.Get(ping + "/start")
    if err := runBackup(); err != nil {
        http.Get(ping + "/fail")
        panic(err)
    }
    http.Get(ping) // success
}

Notice the pattern is identical in every language: signal start, run the job, signal success or failure. One URL. Three endpoints. That's the entire integration.

Best practices

Use /start and /fail, not just success pings

The /start signal lets PingCron measure job duration. The /fail signal gives you instant alerts instead of waiting for the grace period to expire. Both are optional but both are worth the extra line of code.

Set grace periods slightly longer than your longest run

If your backup usually takes 10 minutes but occasionally takes 20, set the grace period to 25 minutes. You want to catch real failures without triggering false alarms on slow runs.

Monitor every scheduled task — especially the "unimportant" ones

The critical jobs are the ones you worry about. The "unimportant" jobs are the ones that silently break and cause problems weeks later. Monitor all of them. The cost of a missed backup is always higher than the cost of a monitor.

Name monitors descriptively

Use names like "db-backup-nightly" or "invoice-sync-15m". When an alert fires at 3 AM, you need to know instantly what broke — not parse a generic name like "job-7".

Test your alerts on day one

After setting up a monitor, deliberately skip a ping and verify the alert arrives on every channel you configured. An alert system you've never tested is an alert system you can't trust.

Set up multiple alert channels

Email alone isn't enough. If your inbox is noisy, the alert gets buried. Configure Slack or Discord so the alert goes where your team actually looks. PingCron lets you enable all channels on the free plan.

What to monitor

If it runs on a schedule, it should have a heartbeat monitor. Here's a non-exhaustive list:

Database backups
Payment processing
Report generation
Data sync jobs
Email campaigns
Cache warming
Log rotation
SSL certificate checks
ETL pipelines
Cleanup scripts
Health checks
API data pulls
WordPress cron
Kubernetes CronJobs
CI/CD pipelines
Invoice processing

The cost of not monitoring

Consider what happens when each of these fails silently:

Database backup failsYou lose weeks of data when the server dies
Payment sync stopsRevenue goes uncollected. Customers get double-charged when it catches up.
SSL renewal failsYour site goes down with a certificate error. Customers see a security warning.
Data sync breaksYour dashboard shows stale data. Decisions get made on wrong numbers.
Cleanup script stopsDisk fills up. Other services crash. 3 AM emergency.

Every one of these is a real scenario that happens to real teams every week. The monitoring that would have caught them takes one line of code and costs nothing on PingCron's free plan.

Start now. Not after the next outage.

Cron jobs will fail. The only question is whether you find out in seconds or in weeks. Heartbeat monitoring is the simplest, most reliable way to ensure you know the moment something breaks.

The integration takes one line of code. The setup takes 30 seconds. The free plan covers 5 monitors with full alerting on email, Slack, Discord, and webhooks.

There is no reason to wait. The next silent failure is already happening.

Start monitoring your cron jobs.

Free plan. 5 monitors. All alert channels. 30-second setup.

Start monitoring free