← Blog  ·  October 2026

Cron Job Health Check: How to Verify Your Scheduled Jobs Are Running

One curl line is all it takes. Here's the pattern, and why it works.

A cron job health check is a URL your job pings at the end of every successful run. If the ping stops arriving — because the job crashed, was skipped, or the server went down — a monitoring service notices and sends you an alert.

This pattern is called a dead man's switch. It's the most reliable way to verify a cron job ran, because it doesn't require reading logs, watching dashboards, or guessing from output files. Silence is the alert.


The pattern in two lines

Add a curl call at the end of any cron job:

# Your existing cron entry — replace the command with yours
*/30 * * * * /path/to/your/script.sh && curl -fsS --retry 3 https://your-monitor.example.com/ping/YOUR-TOKEN > /dev/null 2>&1

The && means the ping only fires if the script exited with code 0. A non-zero exit (failure) means no ping — which triggers the alert. The monitoring service expects a ping within your grace window; when it doesn't arrive, you get notified.

If you prefer keeping the ping unconditional (ping regardless of exit code, so you know the job ran but can track failures separately):

*/30 * * * * /path/to/your/script.sh; curl -fsS --retry 3 https://your-monitor.example.com/ping/YOUR-TOKEN > /dev/null 2>&1

Use ; instead of && to separate the two commands.


How to add a health check to specific environments

Standard crontab

Edit your crontab with crontab -e. Append the curl after your command:

# Append on success:
0 2 * * * /usr/local/bin/backup.sh && curl -fsS --retry 3 https://your-monitor/ping/TOKEN > /dev/null

# Append unconditionally:
0 2 * * * /usr/local/bin/backup.sh; curl -fsS --retry 3 https://your-monitor/ping/TOKEN > /dev/null
Tip: test the curl line manually first Run the curl command in your shell before adding it to cron. Confirm it returns 200 and that your monitoring dashboard shows the ping. Then add it to the crontab.

GitHub Actions scheduled workflow

Add a step at the end of your job:

name: Nightly sync
on:
  schedule:
    - cron: '0 2 * * *'

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run sync
        run: ./scripts/sync.sh

      - name: Health check ping
        if: success()
        run: curl -fsS --retry 3 ${{ secrets.CRONWATCH_PING_URL }}

Store the ping URL as a GitHub Actions secret. The if: success() condition means the ping only fires when all prior steps passed.

AWS EventBridge + Lambda

Add the health check ping as the last call in your Lambda handler:

import urllib.request

def handler(event, context):
    # Your existing logic here
    do_the_work()

    # Health check ping at the end
    urllib.request.urlopen(
        'https://your-monitor/ping/YOUR-TOKEN',
        timeout=5
    )
    return {'statusCode': 200}

Wrap the ping in a try/except so a network blip doesn't fail the Lambda itself:

    try:
        urllib.request.urlopen('https://your-monitor/ping/YOUR-TOKEN', timeout=5)
    except Exception:
        pass  # don't let ping failure break the job

Node.js (node-cron, Agenda, BullMQ)

import cron from 'node-cron';
import fetch from 'node-fetch';

cron.schedule('0 2 * * *', async () => {
  try {
    await doTheWork();
    // Ping on success
    await fetch(process.env.CRONWATCH_PING_URL, { signal: AbortSignal.timeout(5000) });
  } catch (err) {
    console.error('Job failed:', err);
    // No ping = alert fires
  }
});

Python (APScheduler, Celery beat)

import requests

def nightly_job():
    do_the_work()
    try:
        requests.get(
            os.environ['CRONWATCH_PING_URL'],
            timeout=5
        )
    except requests.RequestException:
        pass  # don't let a network blip break the job

scheduler.add_job(nightly_job, 'cron', hour=2)

What to set as the grace period

The grace period is how long the monitoring service waits after the expected ping time before alerting. Set it to roughly 2× your typical job duration:

Job scheduleTypical durationSuggested grace
Every 5 minutes10–30 sec2–5 minutes
Hourly1–5 min10–15 minutes
Daily at 2am5–30 min60 minutes
Weekly (heavy ETL)1–4 hours2 hours

A grace period that's too tight generates false alerts on slow runs. Too loose and you're waiting hours before learning a job missed. When in doubt, start conservative (longer grace) and tighten after you see typical run durations.


Why logging alone isn't enough

A common alternative to health check pings is reading logs: if the job ran, there should be a log entry. The problem:

Health check pings flip the model: instead of looking for evidence a job ran, you get alerted when it didn't. That's a fundamentally more reliable detection pattern.


Verifying the setup works

After adding the ping, do a smoke test:

  1. Run the script manually: ./your-script.sh
  2. Confirm the curl fires and returns 200
  3. Check the monitoring dashboard — you should see a "ping received" event
  4. Reduce the grace period temporarily to 2 minutes
  5. Wait for the alert to fire (confirms the alert path works)
  6. Restore the grace period to the normal value

Step 5 is the one most people skip. If you've never seen an alert fire, you don't know the alert works. Test it intentionally before you're relying on it at 2am.


Ping URL format and what it should return

The ping endpoint should:

The simplest possible implementation you can self-host:

# Express.js — minimal ping receiver
app.get('/ping/:token', async (req, res) => {
  await db.query(
    'UPDATE monitors SET last_ping_at = NOW() WHERE token = $1',
    [req.params.token]
  );
  res.json({ ok: true });
});

You'll also need the background checker that reads last_ping_at and fires an alert when it's overdue. That's roughly another 20–30 lines of code plus email or Slack delivery. Or you can use an existing service that handles the endpoint, storage, and alert routing for you.


⚡ CronWatch — dead man's switch monitoring for cron jobs

CronWatch gives you a ping URL per job. Add one curl line. Get alerted by email the moment a job is late. No infrastructure to run, no dashboards to check.

Free cron expression parser (check your schedule before you add the ping):

Parse my cron expression → Join the CronWatch waitlist →

Summary