Skip to content
Cron Jobs for Developers: Patterns and Best Practices

Cron Jobs for Developers: Patterns and Best Practices

DodaTech Updated Jun 20, 2026 11 min read

Cron is a time-based job scheduler in Unix-like operating systems that executes commands or scripts at specified dates and times using a concise five-field syntax representing minute, hour, day of month, month, and day of week.

What You’ll Learn

By the end of this tutorial, you’ll master cron syntax, learn common scheduling patterns, understand production monitoring and error handling, and know when to use alternatives like systemd timers and Kubernetes CronJob.

Why Cron Jobs Matter

Many tasks must run on a schedule: database backups at 2 AM, daily report generation, certificate renewal every 90 days, and health checks every 5 minutes. Doda Browser uses cron jobs to rotate logs daily, update malware signature databases hourly, and send weekly usage reports to thousands of users.

Cron Syntax Deep Dive


flowchart LR
    A["┌───────── Minute (0-59)
│ ┌───────── Hour (0-23)
│ │ ┌───────── Day of Month (1-31)
│ │ │ ┌───────── Month (1-12)
│ │ │ │ ┌───────── Day of Week (0-7) 0/7=Sun
│ │ │ │ │
* * * * *"] --> B["command
to execute"] style A fill:#f90,color:#fff

Special Characters

CharacterMeaningExampleDescription
*Every0 * * * *Every hour
,List0 9,18 * * *9 AM and 6 PM daily
-Range0 9-17 * * 1-5Every hour 9-5, weekdays
/Step*/15 * * * *Every 15 minutes
LLast (only in some extensions)0 0 L * *Last day of month

Common Cron Patterns

# ── Basic patterns ──

# Every minute
* * * * * /script.sh

# Every 5 minutes
*/5 * * * * /script.sh

# Every 15 minutes
*/15 * * * * /script.sh

# Every hour at minute 0
0 * * * * /script.sh

# Every hour at minute 30
30 * * * * /script.sh

# Daily at midnight
0 0 * * * /script.sh

# Daily at 2:30 AM
30 2 * * * /script.sh

# Weekdays at 9 AM
0 9 * * 1-5 /script.sh

# Once a week (Sunday at 3 AM)
0 3 * * 0 /script.sh

# First day of every month at midnight
0 0 1 * * /script.sh

# Every 6 hours
0 */6 * * * /script.sh

# Every Monday and Thursday at 8 AM
0 8 * * 1,4 /script.sh

Building a Cron Script

# daily_report.py
# Cron-friendly script with logging and error handling

#!/usr/bin/env python3
"""Generate daily analytics report. Run via cron at 6 AM."""
import sys
import json
import logging
from datetime import datetime, timedelta
from pathlib import Path

# Configure logging
LOG_DIR = Path("/var/log/app")
LOG_DIR.mkdir(parents=True, exist_ok=True)
LOG_FILE = LOG_DIR / "daily_report.log"

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(LOG_FILE),
        logging.StreamHandler(sys.stdout),
    ]
)
logger = logging.getLogger(__name__)

def generate_report():
    """Generate daily analytics report from database."""
    logger.info("Starting daily report generation")

    yesterday = datetime.now() - timedelta(days=1)
    report_date = yesterday.strftime("%Y-%m-%d")

    # Simulate data collection
    data = {
        "date": report_date,
        "total_users": 15234,
        "new_signups": 234,
        "active_sessions": 8912,
        "revenue": {
            "subscriptions": 45230.50,
            "one_time": 3200.00,
        },
        "errors": {
            "critical": 0,
            "warning": 12,
            "info": 45,
        }
    }

    # Write report
    report_path = Path(f"/var/reports/daily_{report_date}.json")
    report_path.parent.mkdir(parents=True, exist_ok=True)

    with open(report_path, 'w') as f:
        json.dump(data, f, indent=2)

    logger.info(f"Report saved to {report_path}")
    logger.info(f"Summary: {data['new_signups']} signups, "
                f"${data['revenue']['subscriptions']:.2f} revenue")

    # Return exit code: 0 for success
    return 0

if __name__ == "__main__":
    try:
        exit_code = generate_report()
        sys.exit(exit_code)
    except Exception as e:
        logger.critical(f"Report generation failed: {e}", exc_info=True)
        sys.exit(1)

The corresponding crontab entry:

# Run daily at 6:00 AM
0 6 * * * /usr/bin/python3 /opt/app/scripts/daily_report.py

Cron Alternatives

Systemd Timers

# /etc/systemd/system/daily-report.service
[Unit]
Description=Generate daily analytics report

[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /opt/app/scripts/daily_report.py
User=appuser
Group=appgroup

# Logging
StandardOutput=journal
StandardError=journal
# /etc/systemd/system/daily-report.timer
[Unit]
Description=Run daily report every morning at 6 AM

[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true  # Catch up if missed (e.g., after boot)
RandomizedDelaySec=300  # Random delay 0-5 min to avoid thundering herd

[Install]
WantedBy=timers.target
# Enable and start the timer
sudo systemctl daemon-reload
sudo systemctl enable daily-report.timer
sudo systemctl start daily-report.timer

# Check status
systemctl status daily-report.timer
systemctl list-timers

Kubernetes CronJob

# cronjob.yaml
# Kubernetes CronJob for daily database backup
apiVersion: batch/v1
kind: CronJob
metadata:
  name: db-backup
spec:
  schedule: "0 2 * * *"     # Daily at 2 AM
  timeZone: "America/New_York"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: backup
            image: postgres:16
            command:
            - /bin/sh
            - -c
            - |
              pg_dump -h postgres-svc -U admin mydb | \
              gzip > /backups/$(date +\%Y-\%m-\%d).sql.gz
            env:
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: password
            volumeMounts:
            - mountPath: /backups
              name: backup-storage
          restartPolicy: OnFailure
          volumes:
          - name: backup-storage
            persistentVolumeClaim:
              claimName: backup-pvc
  successfulJobsHistoryLimit: 7
  failedJobsHistoryLimit: 3

AWS EventBridge

# eventbridge-schedule.yaml
# AWS EventBridge scheduled rule (via CloudFormation)
ScheduledRule:
  Type: AWS::Events::Rule
  Properties:
    Name: daily-report-generation
    ScheduleExpression: "cron(0 6 ? * MON-FRI *)"
    State: ENABLED
    Targets:
      - Arn: !GetAtt ReportFunction.Arn
        Id: ReportTarget
        Input: '{"type": "daily", "format": "json"}'

Logging and Monitoring

# cron_monitor.py
# Health check monitor for cron jobs

#!/usr/bin/env python3
"""Monitor cron job health — detect missed or failed runs."""
import json
from pathlib import Path
from datetime import datetime, timedelta
import smtplib
import sys

CRON_LOG_DIR = Path("/var/log/cron-jobs")
ALERT_EMAIL = "admin@example.com"

def check_cron_health():
    """Check all cron jobs ran successfully within expected intervals."""
    jobs = {
        "hourly_cleanup": {"interval_minutes": 70, "max_age_minutes": 90},
        "daily_report": {"interval_minutes": 1440, "max_age_minutes": 1500},
        "db_backup": {"interval_minutes": 1440, "max_age_minutes": 1500},
        "cert_renewal": {"interval_minutes": 43200, "max_age_minutes": 43500},  # 30 days
    }

    now = datetime.now()
    failed_jobs = []

    for job_name, config in jobs.items():
        log_file = CRON_LOG_DIR / f"{job_name}.log"

        if not log_file.exists():
            failed_jobs.append(f"{job_name}: No log file found")
            continue

        # Check last modification time
        mod_time = datetime.fromtimestamp(log_file.stat().st_mtime)
        age_minutes = (now - mod_time).total_seconds() / 60

        if age_minutes > config["max_age_minutes"]:
            failed_jobs.append(
                f"{job_name}: Last run {age_minutes:.0f} minutes ago "
                f"(expected < {config['max_age_minutes']})"
            )

    if failed_jobs:
        alert_message = "CRON HEALTH ALERT\n" + "=" * 40 + "\n"
        alert_message += "\n".join(failed_jobs)
        print(alert_message)
        # Send alert
        # send_alert(alert_message)
        return 1
    else:
        print(f"[OK] All cron jobs healthy at {now.isoformat()}")
        return 0

if __name__ == "__main__":
    sys.exit(check_cron_health())

Error Handling Patterns

# robust_cron.py
# Cron job with comprehensive error handling

#!/usr/bin/env python3
"""Production-grade cron script with error handling."""
import os
import sys
import time
import json
import logging
import subprocess
from pathlib import Path
from datetime import datetime

# Setup
LOCK_DIR = Path("/var/lock/cron")
LOCK_DIR.mkdir(parents=True, exist_ok=True)
JOB_NAME = Path(__file__).stem

def acquire_lock():
    """Prevent concurrent execution (flock-based)."""
    lock_file = LOCK_DIR / f"{JOB_NAME}.lock"
    # Use flock to prevent race conditions
    # In practice: run with `flock -n /var/lock/cron/daily_report.lock`
    return lock_file

def send_alert(subject, body):
    """Send monitoring alert (PagerDuty/Slack/Email)."""
    print(f"[ALERT] {subject}: {body}")
    # Integrate with Slack webhook or PagerDuty API
    # subprocess.run(["curl", "-X", "POST", "-d", json.dumps(body), webhook_url])

def main():
    """Main cron job logic with error handling."""
    start_time = time.time()
    logger = logging.getLogger(JOB_NAME)

    try:
        logger.info(f"Starting {JOB_NAME}")

        # 1. Pre-flight checks
        if not os.getenv("DATABASE_URL"):
            raise EnvironmentError("DATABASE_URL not set")
        if not Path("/var/data").exists():
            raise FileNotFoundError("/var/data directory missing")

        # 2. Business logic
        logger.info("Processing data...")
        time.sleep(2)  # Simulate work
        result = {"processed": 100, "errors": 0, "duration_s": 2.5}

        # 3. Write check-in file (for monitoring)
        checkin = Path(f"/var/checkin/{JOB_NAME}")
        checkin.parent.mkdir(parents=True, exist_ok=True)
        checkin.write_text(json.dumps({
            "status": "success",
            "timestamp": datetime.now().isoformat(),
            "duration": round(time.time() - start_time, 2),
        }))

        logger.info(f"Completed in {time.time() - start_time:.2f}s")
        return 0

    except Exception as e:
        logger.critical(f"Fatal error: {e}", exc_info=True)
        send_alert(
            f"Cron failure: {JOB_NAME}",
            f"Failed at {datetime.now().isoformat()}: {str(e)}"
        )
        return 1

if __name__ == "__main__":
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(message)s",
        handlers=[
            logging.FileHandler(f"/var/log/cron/{JOB_NAME}.log"),
            logging.StreamHandler(sys.stdout),
        ]
    )
    sys.exit(main())

Common Errors

1. Forgetting to Set the PATH

Cron runs with a minimal environment (PATH=/usr/bin:/bin). Your script may fail because python3, node, or other commands aren’t found. Always use absolute paths in cron or set PATH at the top of the crontab.

2. Not Handling Output

Cron sends unhandled output (stdout/stderr) as email to the crontab owner. If no mail system is configured, output is lost. Redirect output to a log file: 0 6 * * * /script.sh >> /var/log/script.log 2>&1.

3. Overlapping Runs

If a job takes longer than its interval, the next instance starts before the first finishes. Use file locking (flock) or a lockfile to prevent concurrent execution.

4. Timezone Confusion

Cron uses the system timezone. If your server is UTC but users are in EST, a “daily at midnight” cron runs at UTC midnight, not EST midnight. Set TZ in crontab or use CRON_TZ.

5. Silent Failures

A script that exits with code 0 despite an error (uncaught exception, incomplete work) is invisible to monitoring. Always check exit codes and use set -e in shell scripts.

6. Daylight Saving Time Gaps

Cron handles DST differently across systems. Some skip jobs during the “spring forward” hour; others run twice during “fall back”. Use UTC for scheduling or accept the ambiguity.

Practice Questions

1. What does */15 * * * * mean?

Every 15 minutes. The */15 is a step value — minute 0, 15, 30, 45 of every hour.

2. How do you prevent a cron job from running multiple times simultaneously?

Use flock -n /var/lock/job.lock before the command. If the lock is held, the new instance exits. This is critical for database backups and other non-idempotent operations.

3. What is the difference between cron and systemd timers?

Cron is simpler but has limited error handling and logging. Systemd timers provide journal integration, dependency management, persistent timers (catch up on missed runs), and random delays. Use systemd for system services, cron for simpler user-level scheduling.

4. How do you write a cron expression for “every weekday at 9:30 AM, but not in December”?

30 9 * 1-11 1-5 /script.sh — runs at 9:30 AM, months January-November, Monday-Friday.

5. Challenge: Design a cron-based system that: (1) runs a health check every 5 minutes, (2) generates a report every hour on the hour, (3) sends a weekly digest every Monday at 8 AM, (4) runs a database backup daily at 2 AM with monitoring that alerts if the backup hasn’t completed by 3 AM.

Use three cron entries + a monitoring script. The backup script writes a timestamp file on completion. A fourth cron at 3 AM checks if the timestamp is recent. If not, it sends an alert. Use flock on the hourly report to prevent overlap if it takes > 1 hour.

Mini Project: Cron Log Analyzer

# log_analyzer.py
# Analyze cron logs for failures and timing patterns

#!/usr/bin/env python3
"""Analyze cron job logs to detect failures and performance trends."""
import re
from pathlib import Path
from datetime import datetime, timedelta
from collections import Counter, defaultdict

LOG_DIR = Path("/var/log/cron")

def analyze_cron_logs(days=7):
    """Parse cron logs and produce a health report."""
    cutoff = datetime.now() - timedelta(days=days)
    job_stats = defaultdict(lambda: {"runs": 0, "failures": 0, "durations": []})

    # Parse log files
    for log_file in sorted(LOG_DIR.glob("*.log")):
        job_name = log_file.stem

        with open(log_file) as f:
            for line in f:
                # Match: "2026-06-20 10:00:00 [INFO] Starting job_name"
                timestamp_match = re.match(
                    r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})", line
                )
                if not timestamp_match:
                    continue

                log_time = datetime.strptime(
                    timestamp_match.group(1), "%Y-%m-%d %H:%M:%S"
                )
                if log_time < cutoff:
                    continue

                job_stats[job_name]["runs"] += 1

                if "ERROR" in line or "CRITICAL" in line:
                    job_stats[job_name]["failures"] += 1

    # Print report
    print(f"{'Job':<20} {'Runs':<8} {'Failures':<10} {'Health':<10}")
    print("-" * 50)

    for job_name, stats in sorted(job_stats.items()):
        failure_rate = stats["failures"] / max(stats["runs"], 1) * 100
        health = "✅" if failure_rate < 1 else "⚠️" if failure_rate < 10 else "❌"
        print(f"{job_name:<20} {stats['runs']:<8} "
              f"{stats['failures']:<10} {health:<10}")

    total_jobs = len(job_stats)
    total_failures = sum(s["failures"] for s in job_stats.values())
    print(f"\nTotal jobs: {total_jobs}")
    print(f"Total failures: {total_failures}")
    print(f"Overall health rate: {(1 - total_failures / max(sum(s['runs'] for s in job_stats.values()), 1)) * 100:.1f}%")

if __name__ == "__main__":
    analyze_cron_logs()

Expected output:

Job                  Runs     Failures   Health
daily_report        7        0          ✅
db_backup           7        0          ✅
hourly_cleanup      168      3          ⚠️
cert_renewal        0        0          ✅

Total jobs: 4
Total failures: 3
Overall health rate: 99.4%

FAQ

What happens if my system is off during a scheduled cron job?
Cron does not run missed jobs when the system comes back online. Use systemd timers with Persistent=true or anacron for jobs that must catch up after downtime.
How do I test a cron expression before deploying?
Use online tools like Crontab Guru (crontab.guru) or crontab -e with a short interval (every 2 minutes) to verify. Always test on a non-production system first.
Can I run cron jobs in a Docker container?
Yes, but the container must run a cron daemon (e.g., crond, supercronic). Many base images don’t include cron. Alternatively, use the host’s cron with docker exec or Kubernetes CronJob.
How do I set environment variables for cron jobs?
Either set them in the crontab file (VAR=value before the command) or use a wrapper script that sources environment variables. For Docker: pass via -e to the container.
What is the maximum frequency for cron jobs?
Every minute (* * * * *). For sub-minute intervals, use a loop with sleep in a long-running script, or use systemd timers with OnCalendar=*-*-* *:*:00 (second granularity).

Related Concepts

What’s Next

You now master cron patterns! Next, explore background job queues for event-driven async processing, then discover serverless scheduling with cloud-native cron alternatives.

  • Practice daily — Set up a cron job that logs system health metrics every 5 minutes
  • Build a project — Build a cron-based backup system with logging, monitoring, and alerting
  • Explore related topics — Check out Apache Airflow for complex workflow orchestration beyond simple cron scheduling

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro