← Blog  ·  October 2026

Heartbeat Monitoring: How to Know When Your Cron Jobs Stop Running

Your nightly backup completed last Tuesday. You'll find out Friday — when someone asks where this week's data is.

Cron jobs run silently. That's the feature. No window, no output, no status indicator. A job that ran successfully at 3am looks exactly like a job that never ran at 3am — unless you instrumented it to say otherwise.

Heartbeat monitoring is the most lightweight way to close that gap. The idea is simple: your job sends an HTTP ping to a monitoring endpoint at the end of each successful run. If the monitoring service doesn't receive a ping within the expected window, it fires an alert. No ping means something went wrong — the job failed, was killed, or never ran at all.


How heartbeat monitoring works

A heartbeat monitor works like a dead man's switch. The monitoring service expects to hear from your job on a predictable schedule. As long as it keeps hearing the ping, everything is fine. The moment the ping goes missing, the alarm fires.

This is fundamentally different from uptime monitoring, which checks whether a URL responds. Heartbeat monitoring checks whether a process completed successfully. It catches failures that uptime probes can't see: a cron job that throws an exception and exits, a job that gets stuck in an infinite loop and never finishes, or a scheduler that stops dispatching jobs entirely.

Implementation is a single line appended to your cron command:

# crontab — ping fires only if the job exits with code 0
0 3 * * * /opt/jobs/backup.sh && curl -fsS --retry 3 https://cron.watch/ping/YOUR-JOB-ID

The && operator ensures the ping only fires on success. If the script exits with a non-zero code, the curl never runs, the monitor doesn't receive the ping, and you get alerted.


What heartbeat monitoring catches that nothing else does

Most monitoring focuses on active signals: an HTTP endpoint returns 200, a service responds to a health check. Heartbeat monitoring inverts this — you're monitoring for the absence of a signal. That inversion lets it catch three failure modes that are otherwise invisible:

1. The job fails silently

Your script runs, encounters an error, and exits with a non-zero code. On Linux crontab, that error goes to the local mail spool, which nobody reads. The job is "running" in the sense that cron dispatched it — but it's not doing anything useful. Without a heartbeat, this can go undetected for days.

2. The job never starts

The scheduler process itself crashes, gets OOM-killed, or fails to restart after a deployment. All jobs that should have run during that window simply don't run. There's no error to catch because nothing happened. Heartbeat monitoring catches this because the expected pings stop arriving.

3. The job exceeds its time window

A data processing job that used to finish in 4 minutes now takes 3 hours because the dataset grew. The server kills it after a timeout. The job doesn't reach the ping at the end, so the monitor fires — even though cron technically "ran" the job. This is the failure mode most teams don't discover until it causes a real outage.

Why a ping at the end, not the start? Pinging at the beginning tells you the job started. Pinging at the end tells you the job completed successfully. Completion is the signal that matters. A job that starts but never finishes is just as broken as a job that never starts.

Adding heartbeat monitoring to common platforms

Linux crontab

# Append the ping with &&
0 2 * * * /usr/bin/python3 /opt/jobs/nightly_sync.py && curl -fsS https://cron.watch/ping/SYNC-JOB

# For jobs that must complete within a time window, use a timeout wrapper
0 2 * * * timeout 3600 /opt/jobs/long_job.sh && curl -fsS https://cron.watch/ping/LONG-JOB

GitHub Actions

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

jobs:
  nightly-sync:
    runs-on: ubuntu-latest
    steps:
      - name: Run sync
        run: python nightly_sync.py

      - name: Heartbeat ping
        if: success()
        run: curl -fsS https://cron.watch/ping/SYNC-JOB

Using if: success() ensures the ping only fires when all prior steps succeeded. If any step fails, the ping is skipped and the monitor alerts you.

AWS Lambda (EventBridge trigger)

import urllib.request

def handler(event, context):
    # your job logic here
    process_records()

    # ping only after successful completion
    urllib.request.urlopen('https://cron.watch/ping/LAMBDA-JOB')
    return {'status': 'ok'}

Kubernetes CronJob

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-sync
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: sync
            image: myapp:latest
            command: ["/bin/sh", "-c", "python sync.py && curl -fsS https://cron.watch/ping/SYNC-JOB"]

Setting the right alert window

When configuring a heartbeat monitor, you set a grace period: how long after the expected ping time should the monitor wait before firing an alert. Too short and you get false positives from jobs that occasionally run a bit slow. Too long and you find out about a failure hours after it happened.

A practical starting point: set the grace period to 10–20% of the job's normal runtime, with a floor of 5 minutes. For a job that normally completes in 10 minutes, a 15-minute grace period is reasonable. For a job that runs in 30 seconds, use 5 minutes.


Handling multi-step jobs

For jobs with multiple stages, you can send intermediate pings to distinguish which stage failed. This requires a monitoring service that supports named check-ins or sequential steps, but the pattern is the same:

#!/bin/bash
set -e

# Stage 1: fetch data
fetch_data.py && curl -fsS "https://cron.watch/ping/DAILY-JOB?msg=fetch_complete"

# Stage 2: process records
process.py && curl -fsS "https://cron.watch/ping/DAILY-JOB?msg=process_complete"

# Stage 3: final sync — completion ping
sync.py && curl -fsS "https://cron.watch/ping/DAILY-JOB"

If stage 2 fails, only the stage 1 ping arrived. You know immediately where the failure occurred without parsing log files.


What to monitor

Not every cron job needs a heartbeat monitor. Prioritize based on impact:

  1. High priority: jobs that affect revenue, billing, customer data, or external SLAs
  2. High priority: jobs that, if missed once, produce incorrect or stale state that compounds over time
  3. Medium priority: jobs that run more than once per hour (high-frequency jobs magnify the cost of a silent failure)
  4. Lower priority: background cleanup jobs with no user-visible impact

If you're unsure, start with your top 3–5 most critical scheduled jobs. Adding a single && and a curl call costs 2 minutes. The first time it catches a failure before a user does, you'll instrument everything else.

📟 Heartbeat monitoring for every cron job

CronWatch monitors your scheduled jobs across every platform — crontab, GitHub Actions, Heroku Scheduler, AWS EventBridge, Kubernetes CronJob. Get alerted in Slack or email within minutes of a missed window or failed run.

Join the CronWatch waitlist → Parse a cron expression

← All articles  ·  CronWatch  ·  Cron expression parser