Cron Job Best Practices: 10 Rules for Reliable Scheduled Tasks

CronBeacon Team · April 2026

Cron jobs are deceptively simple. Write a command, add a schedule, and walk away. But production cron jobs need more care. They run unattended, fail silently, and accumulate subtle problems over time. These ten rules will help you avoid the most common pitfalls.

1. Use absolute paths

Cron jobs run in a minimal environment. The PATH variable in a cron shell is not the same as your interactive shell. Relative paths and commands that depend on PATH resolution are the single most common cause of cron jobs that "work when I run them manually" but fail in production.

Always use full paths for commands and files:

# Bad — relies on PATH
0 2 * * * backup.sh

# Good — explicit path
0 2 * * * /usr/local/bin/backup.sh

2. Redirect output to a log file

By default, cron sends job output to the local mail spool — which almost nobody checks. Redirect both stdout and stderr to a log file so you have a record of what happened:

0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Use >> to append, not > to overwrite. You want a history, not just the last run. Consider log rotation for long-running jobs.

3. Use lock files to prevent overlapping runs

If a job takes longer than its interval, the next execution might start while the previous one is still running. This can cause data corruption, duplicate processing, or resource exhaustion. Use flock to prevent overlapping:

*/5 * * * * /usr/bin/flock -n /tmp/sync.lock /usr/local/bin/sync.sh

The -n flag makes flock exit immediately if the lock is already held, rather than waiting. If the previous run is still going, the new one simply skips.

4. Set appropriate timeouts

A cron job that hangs indefinitely is worse than one that fails. Use the timeout command to ensure jobs don't run forever:

0 2 * * * /usr/bin/timeout 3600 /usr/local/bin/backup.sh

This kills the backup if it takes more than one hour (3600 seconds). Set the timeout to something reasonable for your job — long enough for normal execution, short enough to catch hangs.

5. Monitor with external heartbeats

Logs tell you what happened when a job ran. Heartbeat monitoring tells you when a job didn't run. This is the most important rule: an external service that expects a ping from your job on schedule, and alerts you when the ping doesn't arrive.

0 2 * * * /usr/local/bin/backup.sh && curl -fsS -X POST \
  https://cronbeacon.dev/api/v1/ingestion/check-in \
  -H "Authorization: Bearer $TOKEN"

Without external monitoring, silent failures can go undetected for days or weeks. See our guide on what happens when cron jobs fail silently for real-world examples. For a full walkthrough, read how to monitor cron jobs.

6. Use exit codes correctly

Exit codes are the universal language of success and failure in Unix. Your cron job should exit 0 on success and non-zero on failure. This sounds obvious, but many scripts ignore exit codes from commands they call:

#!/bin/bash
set -euo pipefail  # Exit on error, undefined vars, pipe failures

/usr/local/bin/backup.sh
/usr/local/bin/cleanup-old-backups.sh

Using set -euo pipefail at the top of your script ensures that any failing command immediately stops the script with a non-zero exit code. Without it, the script might silently continue past a failed step.

7. Keep jobs idempotent

An idempotent job is safe to re-run. If the job runs twice (due to a retry, a scheduling overlap, or a manual re-run), the result should be the same as running it once. This means:

  • Use INSERT ... ON CONFLICT instead of plain INSERT for database writes.
  • Check whether work has already been done before doing it again.
  • Use atomic file operations (write to a temp file, then rename) instead of appending to output files.

Idempotency makes your jobs resilient to retries and safer to debug.

8. Use UTC for schedules

Daylight saving time causes two problems for cron: jobs run twice when clocks fall back, and jobs skip when clocks spring forward. Using UTC avoids both issues:

# In your crontab, set the timezone
CRON_TZ=UTC
0 2 * * * /usr/local/bin/backup.sh

UTC never has daylight saving transitions. Your jobs run exactly once at the expected time, every time. This is especially important for jobs that run in the 1-3 AM window, which is where most DST transitions happen. Need help building your cron expression? Our free tool can help.

9. Test in a staging environment first

Don't deploy a new cron job directly to production. Set it up in staging first and verify:

  • The job actually runs on the expected schedule.
  • Paths, permissions, and environment variables are correct.
  • Output and logging work as expected.
  • The job completes within the expected time.
  • Monitoring and alerting fire correctly on failure.

A five-minute test in staging can prevent a week of silent failures in production.

10. Send context with your monitoring

When your job reports a check-in, include metadata about the execution. This gives you context when something goes wrong later:

curl -fsS -X POST \
  https://cronbeacon.dev/api/v1/ingestion/check-in \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "outcome": "success",
    "metadata": {
      "duration_seconds": 42,
      "records_processed": 1583,
      "backup_size_mb": 256
    }
  }'

Metadata like execution duration, record counts, and file sizes helps you spot trends (job is getting slower, processing fewer records) before they become outright failures. For a full list of common cron schedules to use with your monitoring, see cron expression examples.

Start monitoring your cron jobs today.

CronBeacon detects both failed and missing jobs automatically. Set up monitoring in under a minute.

Start Monitoring Free