Cron is a time-based job scheduler that runs commands at specified intervals using a daemon (crond).
Five-field syntax: minute hour day-of-month month day-of-week — each field accepts numbers, ranges, steps, or wildcards.
Crontab files hold job definitions: user crontabs (crontab -e) and system crontabs (/etc/crontab with an extra user field).
Key pitfall: cron runs jobs with minimal environment — never assume PATH or env variables without explicit export.
Performance insight: cron wakes every minute to check schedules — O(1) overhead per job, but overlapping runs corrupt data.
Production insight: silent failures dominate — without logging redirection (>> log 2>&1) a crashed job produces zero feedback.
✦ Definition~90s read
What is cron Jobs in Linux?
A cron job is a single entry in a crontab file that tells the cron daemon to run a command on a schedule. That's it. Nothing magical. The daemon wakes up every minute, scans all the crontabs on the system, matches the current time against every job's schedule, and fires off any that match.
★
Imagine you set a reminder on your phone to water your plants every Monday at 8am.
The confusion usually starts when people conflate 'cron' (the daemon) with 'cron job' (one scheduled unit). If you're debugging why a script ran twice, you're not asking about the daemon — you're asking about a specific job definition. The daemon doesn't care about your feelings. It only cares about the crontab format.
Every user has their own crontab file. System-wide jobs live in /etc/crontab and /etc/cron.d/. The distinction matters because environment variables and execution contexts differ. User crontabs run with that user's shell profile. System crontabs require an explicit user field. Get that wrong and your script silently dies because PATH is empty.
Plain-English First
Imagine you set a reminder on your phone to water your plants every Monday at 8am. You don't have to remember it — your phone just does it automatically, every single week, even while you're asleep. Cron is exactly that, but for your Linux server. You tell it 'run this script at 2am every night', and it does it forever without you lifting a finger. That's it. A tireless, never-forgetting robot assistant built into every Linux system.
Every production system has a graveyard of tasks that someone used to do manually — rotating log files, sending weekly reports, backing up databases, clearing temp folders. Done by hand, these tasks get forgotten, delayed, or skipped on holidays. Done wrong, they take down services. Cron is the Linux answer to this problem: a built-in scheduler that's been running quietly on Unix systems since 1975, and still powers millions of automated workflows today.
The core problem cron solves is reliability. Humans forget. Cron doesn't. If you need something to happen at a predictable time — daily, hourly, every 15 minutes, or at 3:47am on the last day of the month — cron handles it without a process manager, without a paid SaaS tool, and without a single line of application code. It lives at the OS level, which means it works regardless of what language your app is written in or whether your app is even running.
By the end of this article you'll be able to write cron expressions confidently, manage crontab files without breaking things, debug jobs that silently fail, and apply the real-world patterns that DevOps engineers actually use in production. You'll also know the three mistakes that catch almost everyone the first time they use cron — and exactly how to avoid them.
Why Your Backup Script Ran Twice — The Real Story of Cron Jobs in Linux
A cron job is a scheduled task on Linux, driven by the cron daemon (crond). The core mechanic: crond reads crontab files — one per user — and executes commands at times specified by five fields (minute, hour, day of month, month, day of week). That's it. No retry logic, no overlap protection, no alerting. It's a fire-and-forget timer.
In practice, cron jobs run with the environment of the user who owns the crontab, not a full login shell. That means PATH, variables, and even the working directory are minimal — often just /bin:/usr/bin. A script that works interactively may fail under cron because it can't find commands or expects a different $HOME. Also, if a job takes longer than its interval, cron starts a new instance anyway — no locking, no queuing.
Use cron for periodic, stateless, short-lived tasks: log rotation, temp file cleanup, health pings. Avoid it for jobs that must run exactly once, require guaranteed delivery, or need complex dependency chains. For those, you need a scheduler like systemd timers or a distributed queue. Cron is a hammer — don't use it on screws.
⚠ Overlapping Execution
Cron does not prevent overlapping runs. If your job takes 10 minutes and runs every 5, you'll have two concurrent instances — and likely corrupted state.
📊 Production Insight
A team scheduled a database backup every hour via cron. One backup hung due to a slow disk, and subsequent cron instances piled up, exhausting memory and crashing the DB server.
Symptom: OOM killer terminates the backup process mid-write, leaving a corrupted backup file and no alert.
Rule of thumb: Always use file-based locking (flock) or a pidfile inside the cron command to prevent concurrent execution.
🎯 Key Takeaway
Cron is a stateless timer — it does not manage job state, retries, or concurrency.
Always set PATH and other environment variables explicitly inside the crontab or script.
For any job that must not overlap, wrap it with flock or a semaphore — cron won't help you.
thecodeforge.io
Cron Jobs Linux
How Cron Actually Works — The Daemon, the Crontab, and the Schedule
Cron is a daemon — a background process that starts when your system boots and never stops. Its name comes from 'chronos', the Greek word for time. Every minute, the cron daemon wakes up, checks all the crontab files on the system, and asks: 'Is there anything I should run right now?' If yes, it fires the job. Then it goes back to sleep until the next minute.
A crontab (cron table) is just a plain text file that lists scheduled jobs. Each line in a crontab is one job: five time fields followed by the command to run. You never edit this file directly — you use the crontab command, which validates the format before saving, protecting you from syntax errors that would silently break everything.
There are two types of crontabs you'll work with. The first is user crontabs — each Linux user has their own, and jobs run as that user. The second is the system crontab at /etc/crontab and files dropped into /etc/cron.d/, which include an extra field specifying which user to run the job as. For most application-level automation, user crontabs are the right choice. For system-level jobs like log rotation, the system crontab is used.
The key mental model: cron doesn't track whether a previous job finished. If your job takes longer than its schedule interval, you can end up with two copies running at the same time. That's one of the most dangerous production gotchas, and we'll cover how to handle it.
crontab_basics.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# ─────────────────────────────────────────────────
# MANAGINGYOURCRONTAB — the essential commands
# ─────────────────────────────────────────────────
# Open your crontab in the defaulteditor (usually nano or vi)
# This is the ONLY safe way to edit your crontab
crontab -e
# List all your current cron jobs — great for auditing
crontab -l
# RemoveALL your cron jobs (dangerous — no confirmation prompt!)
crontab -r
# Edit the crontab for a specific user (must run as root)
crontab -u deploy_user -e
# ─────────────────────────────────────────────────
# ANATOMYOF A CRONEXPRESSION
# ─────────────────────────────────────────────────
# Each job line follows this exact structure:
#
# ┌──────────── minute (0 - 59)
# │ ┌─────────── hour (0 - 23)
# │ │ ┌──────────── day of month (1 - 31)
# │ │ │ ┌─────────── month (1 - 12)
# │ │ │ │ ┌──────────── day of week (0 - 7, both 0 and 7 = Sunday)
# │ │ │ │ │
# * * * * * command_to_run
# ─────────────────────────────────────────────────
# REALEXAMPLES with plain-English explanations
# ─────────────────────────────────────────────────
# Run database backup every day at 2:30am
302 * * * /opt/scripts/backup_postgres.sh
# Clear the application temp directory every hour
0 * * * * rm -rf /var/app/tmp/*
# Send a weekly analytics report every Monday at 9am
09 * * 1 /opt/scripts/send_weekly_report.py
# Run a health check every 15 minutes
*/15 * * * * /opt/scripts/health_check.sh
# Archive logs on the 1st of every month at midnight
001 * * /opt/scripts/archive_logs.sh
# Run a job only on weekdays (Mon-Fri) at 6am
06 * * 1-5 /opt/scripts/sync_business_data.sh
Output
# Output of: crontab -l
30 2 * * * /opt/scripts/backup_postgres.sh
0 * * * * rm -rf /var/app/tmp/*
0 9 * * 1 /opt/scripts/send_weekly_report.py
*/15 * * * * /opt/scripts/health_check.sh
0 0 1 * * /opt/scripts/archive_logs.sh
0 6 * * 1-5 /opt/scripts/sync_business_data.sh
💡Pro Tip: Use crontab.guru
Before you save any cron expression, paste it into crontab.guru — a free visual editor that translates your expression into plain English in real time. It's saved countless engineers from scheduling jobs at the wrong time.
📊 Production Insight
Cron's per-minute wake cycle adds ~0% CPU overhead, but if your job pool grows beyond a few hundred, consider staggering start times to avoid a thundering herd at minute boundaries.
The @reboot nickname is great but remember it runs before network interfaces are always ready — add a sleep 10 if your job depends on external services.
Rule: always verify with crontab -l after editing — one stray space can shift your schedule by an entire day.
🎯 Key Takeaway
Cron is a simple line-by-line scheduler — five fields plus a command.
It runs as whatever user owns the crontab, with a stripped-down environment.
The most dangerous assumption: that cron knows about your shell customizations.
Choosing the Right Crontab Type
IfJob runs as a single non-root user (e.g., deploy)
→
UseUse a user crontab via crontab -e — simpler, no need to specify user.
IfJob needs to run as different users at different times
→
UseUse system crontab /etc/crontab with the extra user field.
IfJob is a system utility installed via package (e.g., logrotate)
→
UseDrop a script into /etc/cron.daily/ — no crontab editing needed.
Writing Production-Ready Cron Jobs — Logging, Environments, and Locking
Here's where most tutorials stop — and where most real problems start. A cron job that runs date will work fine. A cron job that runs your Python script will almost certainly fail silently the first time, and here's why: cron runs with a minimal environment. It doesn't load your .bashrc, .bash_profile, or any of the environment variables you set in your shell session. That means PATH, PYTHONPATH, NODE_ENV, database credentials, API keys — none of it is there unless you explicitly provide it.
The second production concern is logging. By default, cron swallows all output. If your script crashes, you'll never know unless you've set up logging. The fix is simple: redirect both stdout and stderr to a log file on every single cron job.
The third concern is job overlap. If your backup script takes 90 minutes but runs every hour, you'll eventually have two copies fighting over the same files. The solution is a lock file — a mechanism where the script checks if another copy of itself is already running and exits gracefully if so. The flock utility makes this one line.
These three patterns — explicit environment, output logging, and job locking — are what separate a toy cron job from a production one. The example below shows all three working together in a realistic database backup script.
backup_postgres.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/bin/bash
# ─────────────────────────────────────────────────────────────────
# backup_postgres.sh
# Production-ready cron job: PostgreSQL backup with logging + locking
# Scheduled via crontab to run daily at 2:30am:
# 302 * * * /bin/bash /opt/scripts/backup_postgres.sh
# ─────────────────────────────────────────────────────────────────
# ── 1. EXPLICITENVIRONMENT ──────────────────────────────────────
# Cron's PATH is minimal (/usr/bin:/bin). Set it explicitly so
# commands like pg_dump, aws, gzip are all found correctly.
export PATH="/usr/local/bin:/usr/bin:/bin"
# Load app-specific environment variables from a secure file
# This file contains DB_USER, DB_NAME, S3_BUCKET etc.
# Permissions on this file should be 600 (owner read/write only)
source /etc/app/backup.env
# ── 2. LOGGINGSETUP ─────────────────────────────────────────────
LOG_DIR="/var/log/app_backups"
LOG_FILE="${LOG_DIR}/backup_$(date +%Y-%m-%d).log"TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
# Create log directory if it doesn't exist
mkdir -p "$LOG_DIR"
# RedirectALLoutput (stdout + stderr) to the log file
# The tee command also prints to the terminal when run manually
exec > >(tee -a "$LOG_FILE") 2>&1
echo "[$TIMESTAMP] ─── Backup job started ───"
# ── 3. JOBLOCKING — prevent overlapping runs ────────────────────
LOCK_FILE="/tmp/backup_postgres.lock"
# flock acquires an exclusive lock on the lock file.
# -n means "non-blocking" — if the lock is already held, exit immediately.
# 9 is the file descriptor we're using for the lock.
exec 9>"$LOCK_FILE"if ! flock -n 9; then
echo "[$TIMESTAMP] Another backup is already running. Exiting."
exit 1
fi
# ── 4. THEACTUALWORK ───────────────────────────────────────────
BACKUP_FILENAME="${DB_NAME}_$(date +%Y%m%d_%H%M%S).sql.gz"
BACKUP_PATH="/tmp/${BACKUP_FILENAME}"
echo "[$TIMESTAMP] Dumping database: $DB_NAME"
# pg_dump connects to PostgreSQL and streams SQL to gzip for compression
# PGPASSWORD is read from the sourced env file above
pg_dump -U "$DB_USER" -h "$DB_HOST""$DB_NAME" | gzip > "$BACKUP_PATH"
# Checkif the dump succeeded — never assume it worked
if [ $? -ne 0 ]; then
echo "[$TIMESTAMP] ERROR: pg_dump failed! Aborting upload."
exit 1
fi
echo "[$TIMESTAMP] Dump complete. Uploading to S3: s3://${S3_BUCKET}/"
# Upload to S3 — credentials come from the sourced env file
aws s3 cp "$BACKUP_PATH""s3://${S3_BUCKET}/postgres-backups/${BACKUP_FILENAME}"if [ $? -eq 0 ]; then
echo "[$TIMESTAMP] Upload successful. Cleaning up local file."
rm -f "$BACKUP_PATH"else
echo "[$TIMESTAMP] ERROR: S3 upload failed! Local backup retained at $BACKUP_PATH"
exit 1
fi
echo "[$TIMESTAMP] ─── Backup job completed successfully ───"
Without output redirection, a failing cron job produces zero feedback. Add >> /var/log/myjob.log 2>&1 to every cron line as your absolute minimum. Better yet, build logging directly into the script itself as shown above — that way you get it whether the job is triggered by cron or run manually.
📊 Production Insight
Never store database credentials inside the script file — source them from a separate env file with 600 permissions.
A missing $? check after pg_dump can silently produce empty backups for weeks.
Rule: every script must have exit codes greater than 0 on failure so cron (or a wrapper) can track them via MAILTO or external monitoring.
🎯 Key Takeaway
Three things separate a production cron job from a toy: explicit environment, full logging, and locking.
Without all three, you're one silent failure away from a pager at 3am.
The script pattern above works for any language — just replace the work section.
Choosing the Right Cron Job Structure
IfScript is short (<10 lines) and has no dependencies
→
UseInline command in crontab is fine with >> /var/log/job.log 2>&1.
IfScript is complex, uses env variables, files, or external tools
→
UseWrite a separate script file with full environment setup and logging — as shown in the example above.
IfJob may take longer than its interval
→
UseMust include flock locking — never write a job that can overlap.
thecodeforge.io
Cron Jobs Linux
Special Schedules, System Crontabs, and When to Use Alternatives
Once you're comfortable with the five-field syntax, there are shorthand strings that make common schedules far more readable. Instead of 0 0 * you can write @daily. These are called cron nicknames and every modern cron daemon supports them. They're self-documenting and much harder to misread.
Beyond user crontabs, Linux ships with a set of system-managed cron directories. Drop an executable script into /etc/cron.daily/ and it will run once a day — no crontab editing required. The actual run times are controlled by the run-parts entries in /etc/crontab. These are perfect for system maintenance tasks packaged by software installers.
That said, cron isn't always the right tool. It has real limitations: it has no dependency management (it can't wait for Job A to finish before starting Job B), it doesn't retry on failure, it has no built-in alerting, and it doesn't scale across multiple machines. For anything more complex — multi-step pipelines, distributed systems, or jobs that need retry logic — tools like systemd timers, Apache Airflow, or cloud-native schedulers (AWS EventBridge, GCP Cloud Scheduler) are better fits. Knowing when NOT to use cron is as important as knowing how to use it.
cron_advanced_patterns.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# ─────────────────────────────────────────────────────────────────
# CRONNICKNAMES — readable shorthand for common schedules
# ─────────────────────────────────────────────────────────────────
@reboot /opt/scripts/start_queue_worker.sh # RunONCE at system startup
@hourly /opt/scripts/refresh_cache.sh # Same as: 0 * * * *
@daily /opt/scripts/cleanup_sessions.sh # Same as: 00 * * *
@weekly /opt/scripts/generate_sitemap.sh # Same as: 00 * * 0
@monthly /opt/scripts/invoice_generator.sh # Same as: 001 * *
@yearly /opt/scripts/archive_old_records.sh # Same as: 0011 *
# ─────────────────────────────────────────────────────────────────
# SYSTEMCRONTAB (/etc/crontab) — has an extra USER field
# ─────────────────────────────────────────────────────────────────
# Format: minute hour day month weekday USER command
# Run log rotation as root every day at 6:25am
256 * * * root /usr/sbin/logrotate /etc/logrotate.conf
# Run the app's data sync as the 'deploy' user at midnight
00 * * * deploy /opt/app/bin/sync_data.sh
# ─────────────────────────────────────────────────────────────────
# RUNNING A CRONJOBMANUALLYFORTESTING
# ─────────────────────────────────────────────────────────────────
# The most reliable way to test a cron job is to simulate
# cron's environment: no shell customizations, minimal PATH.
# Run your script exactly as cron would — bare environment
env -i HOME=/root LOGNAME=root PATH=/usr/bin:/bin \
/bin/bash /opt/scripts/backup_postgres.sh
# ─────────────────────────────────────────────────────────────────
# CHECKINGCRONLOGS — when a job runs but you don't see output
# ─────────────────────────────────────────────────────────────────
# On systemd-based systems (Ubuntu20+, RHEL8+)
journalctl -u cron --since "1 hour ago"
# On older systems using syslog
grep CRON /var/log/syslog | tail -50
# OnRedHat / CentOS systems
grep CRON /var/log/cron | tail -50
Output
# journalctl -u cron --since "1 hour ago" output:
Mar 15 02:30:01 prod-server CRON[14823]: (deploy) CMD (/opt/scripts/backup_postgres.sh)
Mar 15 03:00:01 prod-server CRON[15102]: (root) CMD (/usr/sbin/logrotate /etc/logrotate.conf)
Mar 15 03:15:01 prod-server CRON[15341]: (deploy) CMD (/opt/scripts/health_check.sh)
🔥Interview Gold: @reboot Is Underused
The @reboot nickname runs a command once when the system starts. It's perfect for starting background workers, mounting drives, or seeding caches after a reboot — and interviewers love asking whether you know it exists. Unlike init scripts, it requires zero configuration beyond a single crontab line.
📊 Production Insight
When you drop a script into /etc/cron.daily/, it runs at the system's configured time (often 6:25am via anacron or run-parts). If multiple day jobs take longer than 24h, they'll backlog and eventually crash the system.
Systemd timers offer OnCalendar= which is more precise than cron's minute granularity, and they can catch up missed runs with Persistent=true.
Rule: if you need retries or dependency chains, don't try to simulate them in cron — switch to a proper workflow engine.
🎯 Key Takeaway
Cron nicknames make schedules readable — use @daily over 0 0 *.
System crontabs are for system-level jobs, not application scripts.
If you need retries, dependencies, or distributed execution, cron is the wrong tool.
When to Use Alternatives to Cron
IfJob chain with dependencies (Job B only if Job A succeeded)
→
UseUse systemd timer with After= or a workflow engine like Airflow.
IfJob needs retry on failure (e.g., network-bound API call)
→
UseWrap the command in a retry loop, or use systemd Restart=on-failure.
IfJob runs across multiple servers (e.g., once in the fleet)
→
UseUse a distributed scheduler like AWS EventBridge or a CRDT-based approach.
IfSimple periodic command, no dependencies
→
UseCron is perfect — one line, no overhead.
Debugging Cron Jobs — Diagnosing Silent Failures and Common Pitfalls
When a cron job fails silently, there's no error message, no email, no stack trace — just an application that starts degrading while nobody notices. This is the number one reason cron gets a bad reputation in production. The fix is a repeatable debug workflow.
Start with the cron daemon logs. On modern systems, journalctl -u cron shows every job execution — the exact command, the timestamp, and the user. On older systems, /var/log/syslog or /var/log/cron contains the same. Search for your command name with grep.
If the job ran but had no effect, the problem is usually the environment. Simulate cron's minimal environment with env -i HOME=/root PATH=/usr/bin:/bin /bin/bash your_script.sh. This stripped-down shell will reproduce the exact conditions under which cron runs your script. Any errors you see here are the errors cron sees — they just go unlogged.
Another common pitfall: the script has a shebang pointing to the wrong interpreter. If your script starts with #!/usr/bin/python3 but Python 3 is installed at /usr/local/bin/python3, cron will fail with a misleading error. Always use #!/usr/bin/env python3 for portability.
Finally, remember that cron jobs inherit the umask of their parent process — typically 022. If your job creates files that need specific permissions, set umask explicitly in the script.
debug_cron_failures.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# ─────────────────────────────────────────────────────────────────
# DEBUGGING A CRONJOBTHATFAILSSILENTLY
# ─────────────────────────────────────────────────────────────────
# Step1: Checkif cron daemon is even running
ps aux | grep -E 'cron|crond'
# Step2: List your crontab and verify the schedule
crontab -l
# Step3: Check cron logs for recent job starts
journalctl -u cron --since '2 hours ago' | grep -E 'CMD|FAILED'
# Step4: Simulate cron environment and run the script manually
# (Replaces all shell customizations)
env -i HOME="/home/deploy"LOGNAME="deploy"PATH="/usr/bin:/bin"SHELL="/bin/bash" \
/bin/bash /opt/scripts/your_script.sh
# Step5: If the script works in Step4, the issue is PATH or a missing env variable.
# Add explicit exports at the top of the script.
# Step6: Check file permissions on script and all parent directories
ls -la /opt/scripts/your_script.sh
# Step7: Test with a very basic cron job to confirm cron itself works
# (Runfor1 minute from now using 'at' or temporary crontab line)
# Then check cron log
echo "* * * * * echo 'cron works' >> /tmp/cron_test.log" | crontab -
# Wait1 minute then check
cat /tmp/cron_test.log
# Clean up
crontab -r
crontab -l 2>/dev/null || echo "crontab cleaned"
Mar 15 14:30:01 prod-server CRON[17234]: (deploy) CMD (/opt/scripts/your_script.sh)
Mar 15 14:31:02 prod-server CRON[17235]: (deploy) CMD (/opt/scripts/your_script.sh) # shows duplicate run if no lock
# env -i simulation output:
Error: command not found: pg_dump
# -> Missing PATH for postgres binaries
Mental Model
Cron Debugging Mental Model
Cron is like a brand-new Linux install with no configuration — no aliases, no PATH additions, no environment variables.
Every cron job runs in a fresh, isolated shell session with a minimal environment.
Any customisation you rely on must be re-declared inside the script.
Theenv -i command reproduces that isolation exactly — use it to see what cron sees.
When a job works manually but fails under cron, the first suspect is always the environment.
📊 Production Insight
A common scenario: a Python script using os.getenv('ENV') fails because cron doesn't set it. The script exits 0 but does nothing.
Always add a preamble that logs the environment: echo "PATH=$PATH" >> $LOG_FILE at the top of every script.
Rule: if you can't reproduce the failure with env -i, it's not a cron problem — check for race conditions or resource limits.
🎯 Key Takeaway
The cron log is your first line of defense — check it before anything else.
env -i replicates the cron environment perfectly — use it to expose missing variables.
Most cron failures are environment and permissions — not code bugs.
Cron Debug Decision Tree
IfJob appears in cron log but no output
→
UseScript ran but crashed silently — add stderr redirection and test manually.
IfJob does not appear in cron log at all
→
UseCron expression is wrong, crontab corrupted, or daemon stopped.
IfJob appears in log with exit code non-zero
→
UseScript returned an error — check script logic, but also check lock files and permissions.
Real-World Cron Patterns — Database Backups, Cache Warming, and Health Checks
In production, cron jobs fall into a handful of common patterns. Getting these right means the difference between a reliable automation pipeline and a pager at 3am.
Database Backups — The most critical cron job. Use the production-ready pattern from section 2: explicit env, logging, locking. Always validate the backup: after pg_dump, run pg_restore --list on the dump file to catch corruption early. Store backups off-server (S3, NFS) and retain multiple copies.
Cache Warming — Many apps rely on a cache that expires at midnight. Instead of serving slow responses to the first users, run a cron job just before peak traffic (e.g., 0 5 *) to pre-compute and populate the cache. Use flock to prevent overlap if the warming takes longer than the interval.
Health Checks — These run every 5 or 15 minutes and report system health to monitoring (e.g., check disk space, process up, API endpoint response). The script should output metrics in a parseable format (JSON) so Prometheus or a custom collector can ingest them. Unlike backups, health checks should NOT use locking — you want to run even if the previous check is still going (but alert if that happens).
Log Rotation — Cron handles log rotation via /etc/cron.daily/logrotate. When building custom rotation for app logs, never use rm -rf — use logrotate with compression and date-stamped filenames. Accidentally removing a log file that a running process is writing to can cause process hang or data loss.
Data Syncing — For ETL or replication between systems, schedule the sync during off-peak hours. Use idempotent scripts (sync only what changed) and monitor for latency. If a sync fails, cron won't retry — wrap it in a retry loop with exponential backoff.
real_world_patterns.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# ─────────────────────────────────────────────────────────────────
# PATTERN1: DatabaseBackupValidation
# ─────────────────────────────────────────────────────────────────
# Inside the backup script, after dump:
pg_dump -U $DB_USER $DB_NAME | gzip > $BACKUP_PATH.gz
# Validate the backup structure (quick check — doesn't load data)
if ! gzip -dc $BACKUP_PATH.gz | pg_restore --list > /dev/null2>&1; then
echo "[$TIMESTAMP] ERROR: Backup validation failed!"
exit 1
fi
# ─────────────────────────────────────────────────────────────────
# PATTERN2: CacheWarming (e.g., pre-generate popular product pages)
# ─────────────────────────────────────────────────────────────────
# Scheduled at 5am daily
05 * * * /opt/scripts/warm_cache.sh
# warm_cache.sh
#!/bin/bash
for url in $(cat /opt/scripts/top_urls.txt); do
curl -s -o /dev/null -w "%{http_code}""$url" >> /var/log/cache_warm.log 2>&1
done
# ─────────────────────────────────────────────────────────────────
# PATTERN3: HealthCheck with JSONOutput
# ─────────────────────────────────────────────────────────────────
# Every15 minutes
*/15 * * * * /opt/scripts/health_check.sh
# health_check.sh
#!/bin/bash
cat <<EOF
{
"timestamp": "$(date -Iseconds)",
"disk_free_pct": $(df / | awk 'NR==2 {print $5}' | tr -d '%'),
"mem_free_mb": $(free -m | awk 'NR==2 {print $7}'),
"nginx_running": $(pgrep nginx | wc -l)
}
EOF
# The output goes to stdout; redirect in crontab to a log or a file readable by monitoring.
Output
# Output of health_check.sh:
{
"timestamp": "2024-03-15T06:30:01+00:00",
"disk_free_pct": 72,
"mem_free_mb": 1024,
"nginx_running": 1
}
💡Don't Validate Backups with File Size Alone
A corrupted backup can be exactly the same size as a valid one. Use pg_restore --list as shown above — it catches schema corruption, partial dumps, and truncation in milliseconds. For MySQL, use mysqlcheck --databases $DB_NAME after dumping.
📊 Production Insight
Cache warming sounds great until your cache refresh takes longer than 5 minutes and overlaps with itself. Always add flock to cache warmers too — stale cache is better than a runaway refresh loop.
Health checks should never block on external dependencies. If your check pings an external API and that API is slow, your cron job may pile up. Add a timeout: timeout 10 curl ....
Rule: for any cron job that touches production data, add a dry-run mode (--dry-run) that prints what it would do — run it after every code change.
🎯 Key Takeaway
Different cron job types have different patterns: backups need locking, health checks need timeouts, cache warmers need staggering.
Always validate the job's output — a silent success that produces broken data is worse than a crash.
Dry-run mode for every production-affecting cron job. No exceptions.
Choosing a Real-World Cron Job Pattern
IfJob operates on production data (backups, migrations)
→
UseMust include locking, validation, offsite storage, and a dry-run mode. Never test on prod without dry-run.
UseDo NOT lock — you want the check to run even if the previous one is stalled. Set timeouts instead.
IfJob is idempotent (sync data, clean up temp files)
→
UseIdempotency is great, but still log everything — you'll need the logs when something unexpected happens.
What Is a Cron Job? (And Why Your Junior Dev Keeps Calling It 'Cron Job')
A cron job is a single entry in a crontab file that tells the cron daemon to run a command on a schedule. That's it. Nothing magical. The daemon wakes up every minute, scans all the crontabs on the system, matches the current time against every job's schedule, and fires off any that match.
The confusion usually starts when people conflate 'cron' (the daemon) with 'cron job' (one scheduled unit). If you're debugging why a script ran twice, you're not asking about the daemon — you're asking about a specific job definition. The daemon doesn't care about your feelings. It only cares about the crontab format.
Every user has their own crontab file. System-wide jobs live in /etc/crontab and /etc/cron.d/. The distinction matters because environment variables and execution contexts differ. User crontabs run with that user's shell profile. System crontabs require an explicit user field. Get that wrong and your script silently dies because PATH is empty.
CrontabAnatomy.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — devops tutorial
//
// The actual structure behind the curtains
//
// Usercrontab (managed by `crontab -e`):
# ┌───────────── minute (0-59)
# │ ┌───────────── hour (0-23)
# │ │ ┌───────────── day of month (1-31)
# │ │ │ ┌───────────── month (1-12)
# │ │ │ │ ┌───────────── day of week (0-6, Sunday=0 or 7)
# │ │ │ │ │
03 * * * /usr/local/bin/rotate_logs.sh
// Systemcrontab (/etc/crontab) adds a user column:
# minute hour dom month dow user command
03 * * 1-5 root /usr/local/bin/rotate_logs.sh
// /etc/cron.d/ files follow system format:
# Anacron compatible directory for daily tasks
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
03 * * * root /usr/local/bin/rotate_logs.sh 2>/tmp/rotate_err.log
Output
No direct output. Crontab silently accepted.
⚠ Production Trap:
Never pipe crontab -l | crontab - in a deployment script. One syntax error in the pipe and your entire crontab gets nuked. Always validate with crontab -e or a staging environment.
🎯 Key Takeaway
A cron job is one line in a crontab file. The daemon matches time fields exactly — no fuzzy logic, no catch-up. Miss a field and the job never fires.
Cron Syntax Explained — The Five Fields That Control Your Autopilot
Every cron job line has exactly five time fields followed by the command. No exceptions. If someone shows you a six-field line, they're either on a system crontab or they've been reading bad blog posts. The fields are: minute, hour, day of month, month, day of week. Each accepts numbers, ranges, lists, and step values.
The asterisk is a wildcard meaning 'every'. So 0 3 means: run at minute 0 of hour 3, every day of every month, regardless of weekday. That's your 3 AM backup slot. Get a comma in there and you have a list: 0 3,15 runs at 3 AM and 3 PM daily.
Ranges use hyphens:0 9-17 1-5 means every hour on the hour during business hours, Monday through Friday. Steps divide time: /10 fires every ten minutes. Why? Because /10 means 'every 10 minutes starting from 0' — the step operator divides the range and takes every Nth value.
The trap: day-of-week and day-of-month are OR'd together in standard cron, not AND'd. A job scheduled for 0 0 15 * 5 runs on the 15th of every month AND every Friday. If you want strict AND behavior, you need two separate jobs or a wrapper script that checks both conditions.
Output depends on script. Expect logs at /var/log/cron or script-specific stdout/stderr.
🔥Senior Shortcut:
Use crontab.guru to validate your schedule before deployment. One misplaced slash and your 'every hour' becomes 'every minute'. I've seen it.
🎯 Key Takeaway
Five time fields: minute, hour, day-of-month, month, day-of-week. Asterisk means 'every'. Steps (/) divide ranges. Day-of-week and day-of-month are OR'd — test before you trust.
● Production incidentPOST-MORTEMseverity: high
When Database Backup Overlaps Destroyed a Week of Transactions
Symptom
The backup script silently completed without errors, but a week later when a restore was needed, the backup file contained partial, interleaved data from two simultaneous dumps.
Assumption
The engineer assumed cron would wait for the previous job to finish before starting the next one. They'd never seen two copies of the script running at the same time.
Root cause
Cron has no built-in job tracking — it fires the command at the scheduled time regardless of whether a previous invocation is still running. The backup used pg_dump which writes to a temporary file; two concurrent dumps wrote to the same temp file, corrupting it.
Fix
Add an exclusive lock using flock as shown in the production-ready example above. Also switch the backup interval to once daily (the job runtime was well under 24h after optimisation) and set the cron expression to 30 2 * so it runs once per day at 2:30am, when system load is lowest.
Key lesson
Cron will never wait for a previous run to finish — you must implement locking yourself.
Always verify backup integrity with automated restoration tests (e.g., restore to a staging DB and run a checksum).
Log the PID at the start of each run so you can identify overlapping executions retroactively.
Production debug guideSymptom → Action table for the most common cron production issues4 entries
Symptom · 01
Cron job works when run manually but does nothing under cron
→
Fix
Simulate cron's environment: run env -i HOME=$HOME PATH=/usr/bin:/bin /bin/bash your_script.sh and check errors. Compare env output vs cron log.
Symptom · 02
No output from cron job, no log file created
→
Fix
Check cron log: grep your_command /var/log/syslog or journalctl -u cron --since today. Ensure redirection is present: >> /var/log/myjob.log 2>&1.
Symptom · 03
Job runs but produces no effect (e.g., backup file is empty)
→
Fix
Check for overlapping runs: verify lock file existence. If lock file present, script may have exited early due to an unhandled error. Add explicit error handling with exit codes.
Symptom · 04
Job fails with 'command not found' but script uses absolute paths
→
Fix
Check script's shebang line (#!/bin/bash or #!/usr/bin/python3). If shebang is missing, cron assumes shell script and may fail. Also verify script is executable (chmod +x).
★ Quick Debug Cheat Sheet — Cron JobsFive commands to diagnose any cron failure in under 60 seconds
No job output at all−
Immediate action
Open cron log
Commands
sudo journalctl -u cron --since '1 hour ago'
grep CRON /var/log/syslog | tail -30
Fix now
Add >> /var/log/cron_output.log 2>&1 to the crontab line and re-run the job manually.
What Is a Cron Job? (And Why Your Junior Dev Keeps Calling I
CronSyntaxBattleCheatsheet.yml
*/5 * * * * /opt/scripts/check_disk.sh
Cron Syntax Explained
Key takeaways
1
Cron runs with a stripped-down environment
never assume PATH, env variables, or shell customizations are available. Use absolute paths and source env files explicitly inside every script.
2
Always redirect both stdout and stderr to a log file (>> /path/to/job.log 2>&1)
without this, silent failures are guaranteed and you'll have no evidence a job even ran.
3
Use flock to create a lock file when your job's runtime could exceed its schedule interval
running two copies of a backup or migration job simultaneously can corrupt data silently.
4
Know when cron is the wrong tool
if you need retries, job dependencies, distributed execution, or failure alerting, reach for systemd timers, Airflow, or a cloud scheduler instead of bolting complexity onto cron.
5
Validate every backup
size checks are not enough. Use pg_restore --list or similar to catch corruption immediately after dump.
6
Add a dry-run mode to every production-affecting cron job. Test it on every code change before enabling the real schedule.
Common mistakes to avoid
3 patterns
×
Relying on your shell's PATH
Symptom
The job works perfectly when you run it manually but does nothing (or logs 'command not found') when cron triggers it.
Fix
Always use absolute paths in cron commands (/usr/bin/python3 not python3) and set PATH explicitly at the top of your script, or source your env file.
×
Forgetting to redirect stderr
Symptom
Your script throws an error, cron emails root (if mail is configured) or discards it entirely, and you have zero visibility into what went wrong.
Fix
Always append >> /var/log/myjob.log 2>&1 to your crontab line, where 2>&1 merges stderr into stdout so both go to the same log file.
×
Using `crontab -r` when you meant `crontab -e`
Symptom
All your cron jobs disappear instantly with no confirmation prompt and no undo.
Fix
Before you do anything destructive, run crontab -l > ~/crontab_backup.txt to save a copy. Some teams commit their crontab to version control via a provisioning script specifically to prevent this.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
What happens to a cron job if the server is rebooted exactly when the jo...
Q02SENIOR
A developer says their cron job works fine when they run it manually but...
Q03SENIOR
Two cron jobs are scheduled to run at the same time and they both write ...
Q01 of 03SENIOR
What happens to a cron job if the server is rebooted exactly when the job was supposed to run? How would you handle that scenario in production?
ANSWER
Cron does not catch up on missed runs. If the system is off at the scheduled time, that run is lost forever. In production, you can handle this by using systemd timers with Persistent=true, which will run the job immediately after boot if the timer was missed. Alternatively, use @reboot to trigger the job on startup, or design your job to be idempotent and run frequently so missing one run doesn't matter. For critical jobs like database backups, consider a monitoring layer that alerts if a backup file hasn't been created in the last 26 hours.
Q02 of 03SENIOR
A developer says their cron job works fine when they run it manually but fails every time cron triggers it. Walk me through how you'd diagnose and fix that.
ANSWER
First, verify the cron log to confirm the job is actually being triggered. Then reproduce cron's minimal environment by running env -i HOME=/home/user LOGNAME=user PATH=/usr/bin:/bin /bin/bash script.sh. Errors there will mirror what cron sees. Common fixes: set PATH explicitly in the script, use absolute paths for all commands, ensure the script is executable (chmod +x), and check the shebang line. If the script uses environment variables, source them from a dedicated file or set them directly. Also check that file permissions on the script and its parent directories allow the cron user to read and execute.
Q03 of 03SENIOR
Two cron jobs are scheduled to run at the same time and they both write to the same file. What could go wrong, and what's the standard Linux mechanism to prevent it?
ANSWER
Concurrent writes to the same file will cause data corruption — both processes will interleave their output, producing mixed or truncated content. The standard Linux mechanism is file locking via flock. Each script should acquire an exclusive lock on a lock file before writing. If the lock cannot be obtained (another instance is running), the second script should exit with a non-zero status. This prevents overlap entirely. For shared resources like databases, use database-level locking or transaction isolation.
01
What happens to a cron job if the server is rebooted exactly when the job was supposed to run? How would you handle that scenario in production?
SENIOR
02
A developer says their cron job works fine when they run it manually but fails every time cron triggers it. Walk me through how you'd diagnose and fix that.
SENIOR
03
Two cron jobs are scheduled to run at the same time and they both write to the same file. What could go wrong, and what's the standard Linux mechanism to prevent it?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
How do I run a cron job every 5 minutes in Linux?
Use the step syntax with an asterisk: /5 /path/to/your/script.sh. The /5 in the minutes field means 'every 5 minutes' — it's shorthand for 0, 5, 10, 15...55. You can use this step syntax in any of the five time fields.
Was this helpful?
02
Why is my cron job not running even though the syntax looks correct?
The most common causes are: the script isn't executable (run chmod +x /path/to/script.sh), the command uses a PATH that cron doesn't have access to, or the script has a shebang line pointing to the wrong interpreter. Test by running env -i PATH=/usr/bin:/bin /bin/bash /path/to/script.sh to simulate cron's minimal environment and see the real error.
Was this helpful?
03
What's the difference between /etc/crontab and a user crontab?
The system crontab at /etc/crontab has an extra field between the time expression and the command — the username that the command should run as. This allows one file to run jobs as multiple different users. User crontabs (edited with crontab -e) always run as the user who owns them and don't have this extra field. For application-level tasks, prefer user crontabs for security isolation.
Was this helpful?
04
Can cron send me an email when a job fails?
Yes — cron can send email via the system's MTA (like sendmail or postfix) to the user that owns the crontab, or to an address set with the MAILTO variable at the top of the crontab. For example: MAILTO=devops@example.com. However, in modern production environments, this email usually ends up in a spam folder or unmonitored inbox. Better to implement explicit logging and external monitoring (e.g., push metrics to Datadog or Prometheus).
Was this helpful?
05
How do I run a cron job only on weekdays?
Use the day-of-week field with range syntax: 0 6 1-5 /path/to/script.sh. This runs the job at 6am Monday through Friday (1=Monday, 5=Friday). Remember that field 0 and 7 both mean Sunday. You can also use 1,2,3,4,5 but ranges are cleaner.