← Blog  ·  October 2026

Cron Job Monitoring: How to Know When Your Scheduled Jobs Fail

Cron jobs fail silently — no alerts, no error emails, just missing results. Here's how to fix that.

Cron is one of the most reliable pieces of software ever written. It almost never crashes. But the jobs it runs? They fail all the time — and cron doesn't care. It just moves on to the next scheduled run and says nothing.

The result is a class of production failures that nobody notices until a user complains, a report is missing, or a backup that "should have been running for months" turns out to be empty. Cron job monitoring is how you close that gap.

What this guide covers:

1 The problem: cron jobs fail silently

By default, cron captures stdout and stderr and tries to mail them to the local system user. In practice, on most modern servers, that mail never goes anywhere. There's no outbox, no inbox, no notification. The job runs, produces output, and the output disappears.

Even when jobs don't produce output — because they succeed — you still have a monitoring gap: you have no external evidence the job ran at all. A missed run looks identical to a successful run from the outside.

The three failure modes you actually need to catch:


2 What you actually need to monitor

Effective cron job monitoring covers four dimensions:

Last run time

Did the job run in the expected window? A job scheduled at 2am that hasn't been seen since yesterday is a silent failure. This is the most important signal and the one most monitoring tools are built around.

Exit code

Did the job succeed? An exit code of 0 means success; anything else means the job reported failure. This is table stakes but surprisingly often ignored — many people log output without capturing exit codes.

Duration

How long did it take? Duration tracking catches jobs that are hanging (run time increasing over weeks until they hit a timeout), jobs that fail too fast (finished in 1 second when they normally take 5 minutes), and jobs that are degrading (slowly getting slower over time as a table grows).

Missed windows

Did the job miss its schedule entirely — not just fail, but never start? This requires an external observer. If your monitoring system lives on the same server as your cron daemon, a server outage kills both the job and the monitor simultaneously. A missed window check requires an off-host observer.


3 Four approaches to monitor cron jobs in production

Approach 1: Ping / heartbeat monitors (dead man's switch)

This is the most effective approach for most teams. The pattern is simple: your cron job sends an HTTP ping to an external service when it finishes successfully. The external service watches for that ping on a defined schedule. If the ping doesn't arrive in the expected window, it alerts you.

This is called a dead man's switch — the absence of a signal is itself the alert.

Popular services in this space: healthchecks.io, Dead Man's Snitch, and CronWatch. Each works on the same principle: you get a unique URL, you add a curl call to your job, and the service pings you if your job goes quiet.

Why this approach works well:

Approach 2: Log-based monitoring

Pipe cron output to a log file and use a log aggregation tool (Datadog, CloudWatch Logs, Papertrail, Logtail) to alert on patterns:

# In your crontab: redirect all output to a log file
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

You can then set up log-based alerts for error strings, missing log entries over a time window, or unexpected exit codes.

The downside: log-based monitoring only catches failure messages that appear in logs. A missed run produces no log entry at all — you'd need to set up a separate "expected log pattern" alert, which is more complex than it sounds and still doesn't work if the logging agent itself goes down.

Approach 3: Platform-native monitoring

Cloud schedulers often have native observability:

Platform-native monitoring is good when you're already deeply invested in that platform's observability stack. It gets complicated fast if your jobs span multiple platforms.

Approach 4: Homemade monitoring scripts

Roll your own: write a wrapper script that captures exit code, start time, and end time, writes them to a table, and sends you an email or Slack message on failure:

#!/bin/bash
# cron-wrapper.sh — captures exit code and duration, sends Slack alert on failure
JOB_NAME="$1"
shift

START=$(date +%s)
"$@"
EXIT_CODE=$?
DURATION=$(( $(date +%s) - START ))

if [ $EXIT_CODE -ne 0 ]; then
  curl -s -X POST "$SLACK_WEBHOOK_URL" \
    -H "Content-Type: application/json" \
    -d "{\"text\": \"🚨 Cron job *${JOB_NAME}* failed with exit code ${EXIT_CODE} after ${DURATION}s\"}"
fi

exit $EXIT_CODE

Use in crontab:

0 2 * * * /usr/local/bin/cron-wrapper.sh "nightly-backup" /usr/local/bin/backup.sh

This catches failures but not missed runs — the wrapper only fires if the job actually starts. You'd need a second job that checks whether the first job ran recently, which starts to become its own reliability problem.


4 What to look for in a cron job monitoring tool

Feature Why it matters
Alerting latency How quickly does it notify you after a missed run? Some tools check every minute; others poll every 5–15 minutes. A backup job that fails at 2am should alert before 9am when someone notices.
Multi-platform support Can it monitor crontab, Heroku Scheduler, GitHub Actions scheduled workflows, and Kubernetes CronJobs from one dashboard? Most teams end up with jobs on multiple platforms.
Grace periods Jobs don't always run exactly on time — a loaded server may delay a job by a few minutes. Good tools let you set a grace period so you're not paged for a job that ran 90 seconds late.
Alert channels Email, Slack, PagerDuty, SMS. You want alerts to go where your team actually looks, especially at 3am.
Price Most tools charge per monitor. Indie developers and small teams can easily have 20–50 cron jobs. Per-monitor pricing adds up; look for flat-rate plans or generous free tiers.
Ease of setup The best tools require adding a single curl line to an existing job. If setup requires installing an agent, modifying your deployment pipeline, or configuring a sidecar, the activation cost is high enough that it won't happen for most jobs.
The missed-run requirement is non-negotiable: If a monitoring tool only alerts on job failures (non-zero exit codes) and not on missed runs (job never started), it cannot protect you against the most common and most dangerous failure mode. Verify this before committing to a tool.

5 Quick setup: the one-curl-line pattern

The dead man's switch pattern reduces to a single curl call appended to your existing job. Here's the canonical form:

# Pattern: run job, then ping monitor URL on success
0 2 * * * /usr/local/bin/backup.sh && curl -fsS --retry 3 https://monitor-url/your-token

The && means the curl only fires if backup.sh exits 0. If the job fails, no ping is sent, and the monitoring service alerts you after the grace period expires.

Flags explained:

Python

import requests

MONITOR_URL = "https://cronwatch.io/ping/YOUR-TOKEN"

def run_job():
    # Your actual job logic here
    sync_database()
    generate_report()

def main():
    try:
        run_job()
        # Only ping on successful completion
        requests.get(MONITOR_URL, timeout=5)
    except Exception as e:
        print(f"Job failed: {e}")
        # Don't ping — the monitor will alert when the ping is late
        raise SystemExit(1)

if __name__ == "__main__":
    main()

Wrap in a crontab entry:

0 2 * * * /usr/bin/python3 /opt/jobs/sync.py

The monitor URL is called only when run_job() completes without raising. Any unhandled exception causes a non-zero exit, no ping is sent, and the monitoring service alerts you.

Node.js

const MONITOR_URL = process.env.MONITOR_URL || 'https://cronwatch.io/ping/YOUR-TOKEN';

async function runJob() {
  // Your actual job logic here
  await syncDatabase();
  await generateReport();
}

async function main() {
  try {
    await runJob();
    // Only ping on successful completion
    await fetch(MONITOR_URL, { signal: AbortSignal.timeout(5000) })
      .catch(() => {}); // Don't let monitor failure break the job
    process.exit(0);
  } catch (err) {
    console.error('Job failed:', err);
    // Don't ping — the monitor alerts when ping is late
    process.exit(1);
  }
}

main();

For older Node.js without fetch built in, use https.get() or axios:

// axios alternative
const axios = require('axios');
await axios.get(MONITOR_URL, { timeout: 5000 }).catch(() => {});

For jobs where you want to capture both start and finish (to track duration and detect jobs that hang):

# Ping on start, then ping on finish — lets the monitor track duration
0 2 * * * curl -fsS --retry 3 https://monitor-url/your-token/start \
  && /usr/local/bin/backup.sh \
  && curl -fsS --retry 3 https://monitor-url/your-token

This works identically for any scheduler that runs a shell command:

# Heroku Scheduler (Procfile-style)
backup: ./bin/backup.sh && curl -fsS --retry 3 https://monitor-url/your-token

# GitHub Actions scheduled workflow
- name: Run backup
  run: ./bin/backup.sh && curl -fsS --retry 3 https://monitor-url/${{ secrets.MONITOR_TOKEN }}
Don't forget to validate your schedule expression first. A job with a broken cron expression will never run — and a monitoring setup won't help with a schedule you never intended. Use a cron expression parser to confirm the schedule fires when you expect before deploying.

Monitor cron jobs with CronWatch

If you want a purpose-built dead man's switch that works across crontab, GitHub Actions, Heroku Scheduler, and Kubernetes CronJobs — without building and maintaining your own alerting infrastructure — CronWatch is designed exactly for this.

Setup is the single curl-line pattern above. CronWatch handles the schedule tracking, missed-run detection, grace periods, and alerts. You get a dashboard that shows every job's last run time, exit status, and duration history.

📟 Set up cron job monitoring in under a minute

CronWatch monitors your scheduled jobs with dead man's switch detection — alerting you the moment a job misses its window, not after you notice results are missing. Works with crontab, GitHub Actions, Heroku Scheduler, and Kubernetes CronJobs.

Join the CronWatch waitlist → Validate your cron expression

Not sure your cron schedule is correct? Use the free cron expression parser to see the plain-English description and the next five run times before you deploy.


FAQ

What's the difference between monitoring and alerting for cron jobs?

Monitoring is the act of collecting data about job runs — whether they ran, whether they succeeded, and how long they took. Alerting is what happens when the monitoring data shows something wrong: you receive a notification. Both are necessary. Monitoring without alerting is just data nobody looks at. Alerting without monitoring means you're guessing about what actually went wrong.

How do I monitor a cron job that runs on multiple servers?

Use the dead man's switch pattern with a single shared monitor URL. If any one of your servers successfully completes the job, it sends the ping. If all of them fail or none of them run in the expected window, the ping is absent and you get an alert. You don't need per-server monitors — you need a monitor per job, regardless of how many servers run it.

Can I monitor GitHub Actions scheduled workflows?

Yes. Add a final step that curls the monitor URL only on success:

- name: Ping monitor on success
  if: success()
  run: curl -fsS --retry 3 https://cronwatch.io/ping/YOUR-TOKEN

Important: GitHub Actions disables scheduled workflows on repositories with no recent activity (~60 days). A dead man's switch will catch this automatically — the workflow stops running, no ping arrives, you get alerted.

How long should the grace period be?

Set the grace period to about 1.5× the longest the job has ever taken to run. If your nightly backup normally runs in 8 minutes but occasionally takes up to 20, set the grace period to 30 minutes. This prevents false alarms from slow runs while still catching genuinely missed jobs quickly. Start conservative and tighten it once you have a few weeks of run-time data.

What happens if the monitoring service itself is down?

This is a real concern. The better monitoring services publish SLA uptime numbers and run redundant infrastructure. For critical jobs, consider sending pings to two different services (a few extra milliseconds, significant reliability benefit). For most jobs, a single external service with good uptime is sufficient — the alternative (no monitoring) is strictly worse.

Should I monitor every cron job?

No. Focus on jobs whose failure has a user-visible impact or a material cost. Good candidates: data syncs, report generation, billing runs, backup jobs, any job that feeds a customer-facing feature. Skip monitoring for jobs whose failure is immediately obvious (they produce real-time output) or truly inconsequential (e.g., a stats counter that's nice-to-have).

← All articles  ·  CronWatch  ·  Cron parser tool