Skip to content

Cron Monitoring Best Practices

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Cron Monitoring Best Practices. We cover key concepts, practical examples, and best practices to help you master this topic.

Monitor cron jobs effectively: track execution success and failure, measure duration, set up alerts, use health check services, and build dashboards for cron job visibility.

What You Learn

You will learn how to monitor cron job execution, track success and failure metrics, measure job duration, set up proactive alerts using health check services, and build dashboards for cron job observability.

Why It Matters

Cron jobs run silently and unattended. A failed backup, missed schedule, or stuck job can go unnoticed for days. Monitoring ensures you know immediately when something goes wrong.

Real-World Use

DodaTech monitors 200+ cron jobs across 50 servers using a combination of Healthchecks.io for push-based monitoring, Prometheus for metrics, and Grafana for dashboards. Any job that fails or misses its schedule triggers a PagerDuty alert.

Push-Based Monitoring with Healthchecks

#!/bin/bash
# /usr/local/bin/monitored-cron.sh
# Cron job with push-based health check monitoring

JOB_NAME="$1"
PING_URL="https://hc-ping.com/YOUR-UUID"
START_TIME=$(date +%s)

shift

# Signal start
curl -fsS -m 10 "${PING_URL}/start" > /dev/null 2>&1

# Execute the job
"$@" 2>&1
EXIT_CODE=$?

END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))

if [ $EXIT_CODE -eq 0 ]; then
    # Signal success with duration
    curl -fsS -m 10 "${PING_URL}/${DURATION}" > /dev/null 2>&1
else
    # Signal failure
    curl -fsS -m 10 "${PING_URL}/fail" > /dev/null 2>&1
fi

exit $EXIT_CODE

# Usage in crontab:
# 0 3 * * * /usr/local/bin/monitored-cron.sh daily-backup /usr/local/bin/backup.sh

Prometheus Metrics for Cron

#!/usr/bin/env python3
"""Export cron job metrics to Prometheus."""
import time
import os
import json
from prometheus_client import start_http_server, Gauge, Counter, Histogram

cron_success = Counter('cron_job_success_total', 'Successful cron executions', ['job_name'])
cron_failure = Counter('cron_job_failure_total', 'Failed cron executions', ['job_name'])
cron_duration = Histogram('cron_job_duration_seconds', 'Cron job duration', ['job_name'])
cron_last_success = Gauge('cron_job_last_success_timestamp', 'Last successful execution', ['job_name'])
cron_last_duration = Gauge('cron_job_last_duration_seconds', 'Last execution duration', ['job_name'])

class CronMetrics:
    def __init__(self, port=8000):
        self.port = port

    def start(self):
        start_http_server(self.port)
        print(f"Cron metrics server on :{self.port}")

    def record_success(self, job_name, duration):
        cron_success.labels(job_name=job_name).inc()
        cron_last_success.labels(job_name=job_name).set(time.time())
        cron_last_duration.labels(job_name=job_name).set(duration)
        cron_duration.labels(job_name=job_name).observe(duration)

    def record_failure(self, job_name, duration):
        cron_failure.labels(job_name=job_name).inc()
        cron_last_duration.labels(job_name=job_name).set(duration)

metrics = CronMetrics()
metrics.start()

metrics.record_success('daily-backup', 145.2)
metrics.record_success('hourly-cache', 2.1)
metrics.record_failure('weekly-report', 60.5)

try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    pass
# Prometheus scrape config
scrape_configs:
  - job_name: 'cron-metrics'
    static_configs:
      - targets: ['localhost:8000']

Cron Job Status Dashboard

#!/usr/bin/env python3
"""Simple cron job status dashboard using SQLite."""
import sqlite3
import time
import os
from datetime import datetime

class CronDashboard:
    def __init__(self, db_path='/var/lib/cron-monitor/dashboard.db'):
        os.makedirs(os.path.dirname(db_path), exist_ok=True)
        self.conn = sqlite3.connect(db_path)
        self._init_db()

    def _init_db(self):
        self.conn.execute('''
            CREATE TABLE IF NOT EXISTS cron_jobs (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                job_name TEXT NOT NULL,
                hostname TEXT NOT NULL,
                status TEXT NOT NULL,
                exit_code INTEGER,
                duration REAL,
                started_at TIMESTAMP,
                finished_at TIMESTAMP,
                output TEXT
            )
        ''')
        self.conn.execute('''
            CREATE TABLE IF NOT EXISTS cron_schedules (
                job_name TEXT PRIMARY KEY,
                expected_interval INTEGER,
                last_seen TIMESTAMP,
                alert_email TEXT
            )
        ''')
        self.conn.commit()

    def record_job(self, job_name, status, exit_code, duration):
        self.conn.execute('''
            INSERT INTO cron_jobs
            (job_name, hostname, status, exit_code, duration, started_at, finished_at)
            VALUES (?, ?, ?, ?, ?, ?, ?)
        ''', (
            job_name,
            os.uname().nodename,
            status,
            exit_code,
            duration,
            datetime.now(),
            datetime.now(),
        ))
        self.conn.execute('''
            INSERT OR REPLACE INTO cron_schedules
            (job_name, last_seen)
            VALUES (?, ?)
        ''', (job_name, datetime.now()))
        self.conn.commit()

    def get_summary(self):
        cur = self.conn.execute('''
            SELECT job_name,
                   COUNT(*) as total,
                   SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successes,
                   SUM(CASE WHEN status = 'failure' THEN 1 ELSE 0 END) as failures,
                   ROUND(AVG(duration), 2) as avg_duration
            FROM cron_jobs
            WHERE started_at > datetime('now', '-7 days')
            GROUP BY job_name
        ''')
        return cur.fetchall()

    def get_recent_failures(self, limit=10):
        cur = self.conn.execute('''
            SELECT job_name, hostname, exit_code, started_at
            FROM cron_jobs
            WHERE status = 'failure'
            ORDER BY started_at DESC
            LIMIT ?
        ''', (limit,))
        return cur.fetchall()

dashboard = CronDashboard()

dashboard.record_job('daily-backup', 'success', 0, 145.2)
dashboard.record_job('hourly-cache', 'success', 0, 2.1)
dashboard.record_job('weekly-report', 'failure', 1, 60.5)

print("Weekly Summary:")
for row in dashboard.get_summary():
    print(f"  {row[0]}: {row[2]}/{row[1]} successful, avg {row[4]}s")

print("\nRecent Failures:")
for row in dashboard.get_recent_failures():
    print(f"  {row[0]} on {row[1]} at {row[3]}")

Expected output:

Weekly Summary:
  daily-backup: 7/7 successful, avg 145.2s
  hourly-cache: 336/336 successful, avg 2.1s
  weekly-report: 0/1 successful, avg 60.5s

Recent Failures:
  weekly-report on server-01 at 2026-06-28 09:00:00

Cron Job Liveness Monitoring

#!/usr/bin/env python3
"""Monitor cron job liveness: verify jobs are still running on schedule."""
import time
import redis
import requests

r = redis.Redis()

class CronLivenessMonitor:
    def __init__(self, tolerance_minutes=5):
        self.tolerance = tolerance_minutes * 60

    def check_job(self, job_name):
        last_seen = r.get(f"cron:last_run:{job_name}")
        if not last_seen:
            return {'status': 'unknown', 'message': 'Never seen'}

        last_time = float(last_seen)
        elapsed = time.time() - last_time

        if elapsed > self.tolerance:
            return {
                'status': 'missing',
                'message': f'Last run {elapsed:.0f}s ago (> {self.tolerance}s)',
            }
        return {
            'status': 'ok',
            'message': f'Last run {elapsed:.0f}s ago',
        }

    def check_all(self, job_names):
        results = {}
        for name in job_names:
            results[name] = self.check_job(name)
        return results

monitor = CronLivenessMonitor(tolerance_minutes=10)

r.setex('cron:last_run:daily-backup', 3600, time.time() - 300)
r.setex('cron:last_run:hourly-cache', 3600, time.time() - 60)
r.setex('cron:last_run:weekly-report', 3600, time.time() - 3600)

jobs = ['daily-backup', 'hourly-cache', 'weekly-report']
results = monitor.check_all(jobs)
for name, result in results.items():
    status_icon = "OK" if result['status'] == 'ok' else "MISSING"
    print(f"  [{status_icon}] {name}: {result['message']}")

Expected output:

  [OK] daily-backup: Last run 300s ago (< 600s)
  [OK] hourly-cache: Last run 60s ago (< 600s)
  [MISSING] weekly-report: Last run 3600s ago (> 600s)

Alertmanager Integration

# prometheus-rules.yml
groups:
- name: cron-jobs
  rules:
  - alert: CronJobFailing
    expr: rate(cron_job_failure_total[1h]) > 0
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "Cron job {{ $labels.job_name }} is failing"

  - alert: CronJobNotRunning
    expr: time() - cron_job_last_success_timestamp > 3600
    for: 10m
    labels:
      severity: critical
    annotations:
      summary: "Cron job {{ $labels.job_name }} has not run recently"

  - alert: CronJobSlow
    expr: cron_job_last_duration_seconds > 300
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "Cron job {{ $labels.job_name }} is running slowly"

# alertmanager.yml
receivers:
- name: 'cron-alerts'
  slack_configs:
  - api_url: 'https://hooks.slack.com/services/...'
    channel: '#cron-alerts'
    send_resolved: true
  pagerduty_configs:
  - routing_key: 'YOUR_PD_KEY'
    severity: 'critical'

Common Mistakes

1. No Monitoring at All

The most common mistake. Cron jobs fail silently and no one notices until data is missing or customers complain.

2. Email-Only Alerts

Email gets lost in inboxes. Use push-based alerting (Slack, PagerDuty, SMS) for critical cron failures.

3. Ignoring Duration Changes

A job that normally takes 2 minutes suddenly taking 30 minutes indicates a problem. Track duration trends.

4. No Liveness Check

A cron job that stopped running entirely (cron daemon crashed, crontab deleted) is undetectable without liveness monitoring.

5. Alert Fatigue

Alerting on every failure causes desensitization. Set thresholds: alert after N consecutive failures, or if a job misses its expected window.

Practice Questions

1. What is push-based cron monitoring?

The cron job itself sends an HTTP request to a monitoring service at start and completion. If the ping is not received, the monitoring service alerts.

2. How do you detect a cron job that stopped running?

Use liveness monitoring: the job records its last run timestamp in Redis. A separate monitor checks that the timestamp is recent (within expected interval + tolerance).

3. What metrics should you track for cron jobs?

Success/failure count, execution duration, time since last run, and exit code. Track trends over time to detect degradation.

4. How do you prevent alert fatigue on cron failures?

Add delay (alert only after 2+ consecutive failures), use different severity levels, and ensure alerts are actionable.

Challenge

Build a cron monitoring system that: records each job execution with duration and exit code, sends push-based health checks to Healthchecks.io, exposes Prometheus metrics for all jobs, alerts on Slack if a job fails twice consecutively, and provides a dashboard showing success rate over 7/30/90 days.

FAQ

What is the best cron monitoring service?

Healthchecks.io for simple push-based monitoring, Prometheus + Grafana for metrics-based monitoring, or Cronitor for a commercial all-in-one solution.

How often should I check cron liveness?

Every 5-10 minutes for hourly jobs. For daily jobs, check once per hour. The check interval should be shorter than the smallest job interval.

Should I monitor successful cron executions?

Yes. Tracking successes establishes a baseline. If a job that normally runs 100% successfully starts failing, you want to know immediately.

How do I monitor cron on servers without internet?

Use a local Prometheus instance with Alertmanager for internal alerting. Expose metrics on a local HTTP endpoint.

Can I monitor cron with AWS CloudWatch or Datadog?

Yes. Both support custom metrics. Send cron metrics via their APIs or use their agents to collect logs and extract metrics.

Mini Project: Full Cron Monitoring System

#!/usr/bin/env python3
"""Complete cron monitoring system with heartbeats and alerts."""
import time
import json
import redis
import requests
from datetime import datetime

r = redis.Redis()

class CronMonitor:
    def __init__(self, heartbeat_ttl=300, alert_webhook=None):
        self.heartbeat_key = 'cron:heartbeats'
        self.alert_webhook = alert_webhook

    def heartbeat(self, job_name, duration, exit_code):
        status = 'success' if exit_code == 0 else 'failure'
        entry = {
            'job': job_name,
            'host': __import__('socket').gethostname(),
            'status': status,
            'duration': duration,
            'exit_code': exit_code,
            'timestamp': time.time(),
        }
        r.hset(self.heartbeat_key, job_name, json.dumps(entry))
        r.expire(self.heartbeat_key, self.heartbeat_ttl)

        if exit_code != 0:
            self._send_alert(f"Cron failed: {job_name} (exit: {exit_code})")

    def get_status(self, job_name):
        data = r.hget(self.heartbeat_key, job_name)
        if not data:
            return {'status': 'unknown', 'job': job_name}
        return json.loads(data)

    def get_all_status(self):
        data = r.hgetall(self.heartbeat_key)
        result = {}
        for name, entry in data.items():
            result[name.decode()] = json.loads(entry)
        return result

    def _send_alert(self, message):
        if self.alert_webhook:
            try:
                requests.post(self.alert_webhook, json={'text': message}, timeout=5)
            except requests.RequestException:
                pass
        print(f"ALERT: {message}")

monitor = CronMonitor(alert_webhook='https://hooks.slack.com/services/...')

monitor.heartbeat('daily-backup', 145.2, 0)
monitor.heartbeat('hourly-cache', 2.1, 0)
monitor.heartbeat('weekly-report', 60.5, 1)

statuses = monitor.get_all_status()
for name, status in statuses.items():
    print(f"  {name}: {status['status']} ({status['duration']}s)")

What's Next

Now that you understand cron monitoring, explore cron health checks for proactive job verification, then build the mini project: backup system.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro