If you manage Linux servers, cron jobs are one of the most powerful tools in your arsenal. They let you automate repetitive tasks — backups, log rotation, certificate renewal, health checks — so they run on schedule without you lifting a finger.
This guide covers everything: how cron works, the full syntax, 20 real-world examples, common pitfalls, advanced locking techniques, systemd timers as a modern alternative, and a security model you should follow on production servers.
By the end, you will have a solid mental model of Linux task scheduling and a cheat sheet you can refer to forever.
1. Understanding Cron
What Is the Cron Daemon?
cron is a time-based job scheduler built into virtually every Linux distribution. It runs as a background daemon (crond on RHEL/CentOS, cron on Debian/Ubuntu) and wakes up every minute to check whether any scheduled jobs need to run.
The name comes from the Greek word chronos (time). Cron has been part of Unix since the 1970s — it is battle-tested, universally available, and extremely reliable for what it does.
How Cron Works Behind the Scenes
- The cron daemon starts at boot and runs continuously in the background.
- Every minute, it reads all crontab files from the spool directory and the system crontab.
- It compares the current time against every job definition.
- If the current time matches the schedule expression, it forks a child process and runs the command.
- By default, any output (stdout/stderr) is mailed to the crontab owner unless redirected.
You can verify the daemon is running with:
systemctl status cron # Debian/Ubuntu
systemctl status crond # RHEL/CentOS/AlmaLinux
Cron vs Systemd Timers
Modern Linux systems (Ubuntu 16.04+, Debian 9+) ship with systemd, which includes its own timer units as an alternative to cron. Here is a brief comparison:
| Feature | Cron | systemd Timers |
|---|---|---|
| Availability | All Linux distributions | Systemd-based distros only |
| Logging | syslog only | journald (full structured logs) |
| Dependencies | None | Can depend on other units |
| Missed jobs | Skipped silently | Can catch up on missed runs |
| Randomized delay | No | Yes (prevents thundering herd) |
| Complexity | Single line | Two unit files required |
| Learning curve | Very low | Moderate |
Rule of thumb: Use cron for simple, standalone tasks. Use systemd timers when you need proper logging, dependency ordering, or catch-up behavior for missed runs.
2. Crontab Syntax Explained
Every cron job entry has five time fields followed by the command to run:
* * * * * command_to_run
│ │ │ │ │
│ │ │ │ └── Day of week (0–7) Sun=0 or 7
│ │ │ └──── Month (1–12)
│ │ └────── Day of month (1–31)
│ └──────── Hour (0–23)
└────────── Minute (0–59)
Special Characters
| Character | Meaning | Example |
|---|---|---|
* |
Any / every value | * in minute = every minute |
, |
List of values | 0,15,30,45 = every 15 minutes |
- |
Range of values | 1-5 in day-of-week = Mon–Fri |
/ |
Step values | */5 in minute = every 5 minutes |
Special Strings (Shortcuts)
These replace the five time fields entirely:
| String | Equivalent | Meaning |
|---|---|---|
@reboot |
— | Run once at system startup |
@yearly |
0 0 1 1 * |
Once a year, Jan 1 at midnight |
@annually |
0 0 1 1 * |
Same as @yearly |
@monthly |
0 0 1 * * |
Once a month, 1st day at midnight |
@weekly |
0 0 * * 0 |
Once a week, Sunday at midnight |
@daily |
0 0 * * * |
Once a day at midnight |
@midnight |
0 0 * * * |
Same as @daily |
@hourly |
0 * * * * |
Once an hour at the top of the hour |
Syntax Examples
| Schedule | Meaning |
|---|---|
* * * * * |
Every minute |
0 * * * * |
Every hour (at :00) |
0 2 * * * |
Every day at 2:00 AM |
0 2 * * 0 |
Every Sunday at 2:00 AM |
0 9 * * 1-5 |
Monday–Friday at 9:00 AM |
*/15 * * * * |
Every 15 minutes |
0 */6 * * * |
Every 6 hours (midnight, 6, 12, 18) |
30 4 1,15 * * |
4:30 AM on the 1st and 15th of each month |
0 22 * * 1-5 |
Weekdays at 10:00 PM |
23 0-20/2 * * * |
Every 2 hours from midnight to 8 PM, at :23 |
3. Managing Crontab
Each Linux user has their own crontab file. The crontab command is how you manage it.
Essential Commands
# Edit the current user's crontab (opens in $EDITOR or nano/vi)
crontab -e
# List the current user's crontab entries
crontab -l
# Remove the current user's crontab entirely (DANGEROUS — no confirmation!)
crontab -r
# Edit another user's crontab (requires root)
sudo crontab -u www-data -e
# List another user's crontab
sudo crontab -u deploy -l
# Edit root's crontab
sudo crontab -e
Warning:crontab -randcrontab -eare one letter apart. One removes your entire crontab without confirmation. If you have important jobs, always keep a backup copy.
Choosing Your Editor
Set the default editor for crontab with the EDITOR environment variable:
export EDITOR=nano
crontab -e
Or set it permanently in ~/.bashrc:
echo 'export EDITOR=nano' >> ~/.bashrc
source ~/.bashrc
Environment Variables in Crontab
You can set environment variables at the top of your crontab file:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
[email protected]
HOME=/root
# Jobs below this line
0 2 * * * /opt/scripts/backup.sh
- MAILTO — Where to send job output. Set to empty string (
MAILTO="") to suppress all mail. - PATH — Cron runs with a minimal PATH. Always define it explicitly.
- SHELL — Default is
/bin/sh. Set to/bin/bashif your scripts use bash-specific syntax.
4. Cron Directories
Beyond user crontabs, Linux has a system-level cron infrastructure:
/etc/crontab — System Crontab
Unlike user crontabs, /etc/crontab has an extra USERNAME field:
# m h dom mon dow user command
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
/etc/cron.d/ — Drop-In Cron Files
Package managers and applications drop cron files here instead of modifying /etc/crontab. Same format as /etc/crontab (includes USERNAME field).
# Example: /etc/cron.d/certbot
0 */12 * * * root python3 -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew -q
Run-Parts Directories
Drop executable shell scripts into these directories and they run automatically:
/etc/cron.hourly/— Run every hour/etc/cron.daily/— Run once per day/etc/cron.weekly/— Run once per week/etc/cron.monthly/— Run once per month
Scripts must be executable (chmod +x) and must not have file extensions (no .sh — run-parts skips files with dots in the name).
# Create a daily cleanup script
cat > /etc/cron.daily/clean-tmp << 'EOF'
#!/bin/bash
find /tmp -type f -atime +7 -delete
find /tmp -type d -empty -delete
EOF
chmod +x /etc/cron.daily/clean-tmp
/var/spool/cron/crontabs/ — User Crontabs on Disk
User crontab files are stored here. Do not edit them directly — always use crontab -e, which validates syntax before saving.
ls -la /var/spool/cron/crontabs/
5. 20 Practical Cron Job Examples
1. Backup Database Every Night at 2 AM
0 2 * * * /usr/bin/mysqldump -u root -pSECRET mydb | gzip > /backups/mydb-$(date +\%Y\%m\%d).sql.gz
Note the \% escaping — in crontab, unescaped % is treated as a newline. Always escape percent signs.
2. Clear /tmp Every Sunday at 3 AM
0 3 * * 0 find /tmp -type f -atime +7 -delete 2>/dev/null
3. Renew SSL Certificates Monthly
0 3 1 * * /usr/bin/certbot renew --quiet --post-hook "systemctl reload nginx" >> /var/log/certbot-renew.log 2>&1
4. Check Disk Space Every Hour, Alert If Over 90%
0 * * * * USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%'); if [ "$USAGE" -gt 90 ]; then echo "ALERT: Disk at ${USAGE}% on $(hostname)" | mail -s "Disk Alert" [email protected]; fi
5. Rotate Logs Weekly
0 0 * * 0 /usr/sbin/logrotate /etc/logrotate.conf --force >> /var/log/logrotate-manual.log 2>&1
6. Sync Files to Remote Server Every 6 Hours
0 */6 * * * /usr/bin/rsync -az --delete /var/www/html/ [email protected]:/backups/html/ >> /var/log/rsync.log 2>&1
7. Restart a Service If It Crashes (Health Check)
*/5 * * * * systemctl is-active --quiet nginx || systemctl restart nginx
This checks every 5 minutes and restarts Nginx silently if it is not running.
8. Update System Packages Weekly With Logging
0 4 * * 0 apt-get update -qq && apt-get upgrade -y -qq >> /var/log/auto-updates.log 2>&1
9. Send Daily Server Report Email
0 7 * * * /opt/scripts/daily-report.sh | mail -s "Daily Report: $(hostname)" [email protected]
The script can include uptime, memory usage, disk, recent errors — whatever you find useful.
10. WordPress: Replace wp-cron with Real Cron
*/10 * * * * /usr/bin/wp --path=/var/www/wordpress cron event run --due-now --allow-root >> /var/log/wp-cron.log 2>&1
Disable the built-in HTTP-triggered wp-cron by adding this to wp-config.php:
define('DISABLE_WP_CRON', true);
11. Clean Old Backups (Keep Last 7 Days)
0 5 * * * find /backups -name "*.sql.gz" -mtime +7 -delete && find /backups -name "*.tar.gz" -mtime +7 -delete
12. Monitor Website Uptime Every 5 Minutes
*/5 * * * * curl -sf https://yourdomain.com > /dev/null || echo "DOWN: yourdomain.com at $(date)" | mail -s "Site Down Alert" [email protected]
13. Compress Access Logs Daily
0 1 * * * gzip -9 /var/log/nginx/access.log && mv /var/log/nginx/access.log.gz /backups/logs/access-$(date +\%Y\%m\%d).log.gz && systemctl reload nginx
14. Database Optimization Weekly
For MySQL:
0 3 * * 0 /usr/bin/mysqlcheck -u root -pSECRET --all-databases --optimize -q >> /var/log/mysql-optimize.log 2>&1
For PostgreSQL:
0 3 * * 0 sudo -u postgres /usr/bin/vacuumdb --all --analyze --quiet >> /var/log/pg-vacuum.log 2>&1
15. Git Pull Deploy Every 10 Minutes
*/10 * * * * cd /var/www/myapp && git pull origin main >> /var/log/git-deploy.log 2>&1 && systemctl reload php8.3-fpm
16. Run Once at Boot
@reboot /opt/scripts/startup-init.sh >> /var/log/startup.log 2>&1
Useful for starting custom daemons, mounting drives, or pre-warming caches on boot.
17. Ban Failed SSH Attempts Report
0 8 * * * grep "Failed password" /var/log/auth.log | awk '{print $11}' | sort | uniq -c | sort -rn | head -20 | mail -s "SSH Brute Force Report: $(hostname)" [email protected]
18. SSL Certificate Expiry Check Weekly
0 9 * * 1 for domain in yourdomain.com mail.yourdomain.com; do EXPIRY=$(echo | openssl s_client -servername $domain -connect $domain:443 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2); echo "$domain expires: $EXPIRY"; done | mail -s "SSL Expiry Report" [email protected]
19. Multiple Schedules Using Comma Separation
# Run at 8 AM, 12 PM, and 6 PM every day
0 8,12,18 * * * /opt/scripts/check-api.sh
# Run on the 1st and 15th of each month
0 2 1,15 * * /opt/scripts/billing-reconcile.sh
20. Temperature Monitoring (Dedicated Servers)
*/10 * * * * TEMP=$(sensors | grep "Package id 0" | awk '{print $4}' | tr -dc '0-9.'); if [ $(echo "$TEMP > 85" | bc) -eq 1 ]; then echo "HIGH TEMP: ${TEMP}C on $(hostname)" | mail -s "Temperature Alert" [email protected]; fi
6. Common Mistakes and Troubleshooting
Mistake 1: PATH Is Too Minimal
Cron runs with a stripped-down PATH:
/usr/bin:/bin
This means commands like php, node, wp, certbot are often not found. Always use absolute paths or define PATH at the top of your crontab.
# Wrong
* * * * * php /var/www/script.php
# Correct
* * * * * /usr/bin/php /var/www/script.php
# Or define PATH at top of crontab
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
* * * * * php /var/www/script.php
Mistake 2: Environment Variables Not Loaded
Cron does not source ~/.bashrc, ~/.bash_profile, or /etc/environment. If your script needs environment variables, load them explicitly:
0 2 * * * source /etc/environment && /opt/scripts/backup.sh
# Or set them inside the script
export DATABASE_URL="postgres://..."
Mistake 3: Script Not Executable
# Check permissions
ls -la /opt/scripts/backup.sh
# Fix if needed
chmod +x /opt/scripts/backup.sh
Mistake 4: Output Not Captured
By default, cron emails output to the crontab owner. If you have no mail server, output is silently lost.
# Capture both stdout and stderr to a log file
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# Discard all output (not recommended for production)
0 2 * * * /opt/scripts/backup.sh > /dev/null 2>&1
# Log with timestamps
0 2 * * * echo "=== $(date) ===" >> /var/log/backup.log && /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
Mistake 5: Timezone Confusion
Cron uses the system timezone by default. If your server is in UTC but you want jobs in your local time:
# Check system timezone
timedatectl
# Set timezone per crontab (requires vixie-cron or compatible)
CRON_TZ=America/New_York
0 9 * * * /opt/scripts/morning-report.sh
Mistake 6: The crontab -r Disaster
One wrong keystroke and your entire crontab is gone with zero confirmation. Prevent this:
# Back up your crontab before editing
crontab -l > ~/crontab.backup.$(date +%Y%m%d)
crontab -e
# Restore if needed
crontab ~/crontab.backup.20261201
Mistake 7: Percent Signs in Commands
In crontab, unescaped % characters are treated as newlines (end of command). Escape them:
# Wrong — the date command will fail
0 2 * * * mysqldump mydb > /backup/mydb-$(date +%Y%m%d).sql
# Correct
0 2 * * * mysqldump mydb > /backup/mydb-$(date +\%Y\%m\%d).sql
Debugging Cron Jobs
# Check if cron ran your job
grep CRON /var/log/syslog
grep CRON /var/log/auth.log
# Live view of cron activity
tail -f /var/log/syslog | grep CRON
# Test your command manually as the cron user
sudo -u www-data bash -c 'PATH=/usr/bin:/bin && /opt/scripts/myjob.sh'
7. Advanced: Preventing Overlapping Jobs With flock
If a cron job takes longer than its interval, the next run starts before the previous one finishes. This can cause race conditions, corrupted files, or runaway processes.
The Problem
# If this job takes 3 minutes and runs every minute, you get 3 concurrent instances
* * * * * /opt/scripts/process-queue.sh
The Solution: flock
flock acquires an exclusive lock on a file before running your command. If the lock is already held, the new instance exits immediately.
# -n = non-blocking (fail immediately if lock is held)
* * * * * /usr/bin/flock -n /tmp/process-queue.lock /opt/scripts/process-queue.sh
# With logging to know when jobs were skipped
* * * * * /usr/bin/flock -n /tmp/process-queue.lock -c '/opt/scripts/process-queue.sh || echo "$(date): Job skipped (already running)" >> /var/log/process-queue.log'
# -w 30 = wait up to 30 seconds for lock before giving up
*/5 * * * * /usr/bin/flock -w 30 /tmp/myjob.lock /opt/scripts/long-job.sh
flock in Shell Scripts
You can also add locking inside the script itself using a subshell:
#!/bin/bash
LOCK_FILE="/tmp/$(basename $0).lock"
exec 200>"$LOCK_FILE"
flock -n 200 || { echo "Another instance is running. Exiting."; exit 1; }
# Your script logic here
echo "Processing..."
sleep 10
echo "Done."
8. Advanced: Systemd Timers as a Modern Alternative
For production workloads requiring proper logging and dependency management, systemd timers are worth the extra setup.
Creating a Systemd Timer: Two-File Setup
You need a service unit and a timer unit. Example: run a backup script every night at 2 AM.
Step 1: Create the service unit
# /etc/systemd/system/nightly-backup.service
[Unit]
Description=Nightly Database Backup
After=network.target mysql.service
[Service]
Type=oneshot
User=root
ExecStart=/opt/scripts/backup.sh
StandardOutput=journal
StandardError=journal
Step 2: Create the timer unit
# /etc/systemd/system/nightly-backup.timer
[Unit]
Description=Run Nightly Backup at 2 AM
Requires=nightly-backup.service
[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=300
Persistent=true
[Install]
WantedBy=timers.target
Step 3: Enable and start the timer
systemctl daemon-reload
systemctl enable --now nightly-backup.timer
Useful Timer Commands
# List all active timers (shows next run time)
systemctl list-timers
# Check timer status
systemctl status nightly-backup.timer
# View logs for the service
journalctl -u nightly-backup.service -n 50
# Run the service immediately (for testing)
systemctl start nightly-backup.service
OnCalendar Syntax Examples
OnCalendar=daily # Every day at midnight
OnCalendar=weekly # Every Monday at midnight
OnCalendar=monthly # First day of each month
OnCalendar=hourly # Every hour
OnCalendar=*-*-* 02:30:00 # Every day at 2:30 AM
OnCalendar=Mon..Fri 09:00:00 # Weekdays at 9 AM
OnCalendar=*-*-1,15 00:00:00 # 1st and 15th of each month
OnCalendar=Sun *-*-* 04:00:00 # Every Sunday at 4 AM
Key Advantages of Systemd Timers
- Persistent=true — If the machine was off when a job was scheduled to run, it runs at the next boot. Cron silently skips missed jobs.
- RandomizedDelaySec — Adds a random delay (e.g., 0-5 minutes) to prevent many timers from firing simultaneously on server clusters.
- Full journald logging — Every run is logged with timestamps, exit codes, and full output. Queryable with
journalctl. - Dependencies — A timer can wait for a database service to be up before running.
9. Logging and Monitoring Cron Jobs
System Log (Default)
# Cron activity goes to syslog
grep CRON /var/log/syslog | tail -50
# Filter by specific time
grep "Mar 28 02:00" /var/log/syslog | grep CRON
Custom Log Files
# Append all output to a rotating log
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# Add timestamps to every log line
0 2 * * * { echo "=== START $(date) ==="; /opt/scripts/backup.sh; echo "=== END $(date) ==="; } >> /var/log/backup.log 2>&1
Email Notifications (MAILTO)
# Set at the top of crontab
[email protected]
# Suppress email for specific jobs
0 * * * * /opt/scripts/quiet-job.sh > /dev/null 2>&1
# Override MAILTO for one job
[email protected]
0 3 * * * /opt/scripts/critical-backup.sh
[email protected]
Healthchecks Pattern
Services like healthchecks.io let you ping a URL at the end of each cron job. If the ping does not arrive within the expected window, you get an alert.
# Ping a healthcheck URL after successful completion
0 2 * * * /opt/scripts/backup.sh && curl -fsS --retry 3 https://hc-ping.com/YOUR-UUID > /dev/null
10. Security Best Practices
cron.allow and cron.deny
Control which users can schedule cron jobs:
# Only users listed in cron.allow can use cron
# If cron.allow does not exist, cron.deny is checked
# If neither exists, only root can use cron (depends on distro)
# Allow only specific users
echo "deploy" >> /etc/cron.allow
echo "www-data" >> /etc/cron.allow
# Block specific users
echo "untrusted-user" >> /etc/cron.deny
Never Store Credentials in Crontab
Do not put passwords directly in crontab entries or scripts visible to other users:
# Wrong — password visible in crontab
0 2 * * * mysqldump -uroot -pMY_PASSWORD mydb > /backup/db.sql
# Correct — use MySQL options file (chmod 600)
0 2 * * * mysqldump --defaults-extra-file=/root/.my.cnf mydb > /backup/db.sql
/root/.my.cnf:
[client]
user=root
password=MY_PASSWORD
chmod 600 /root/.my.cnf
Always Use Absolute Paths
# Wrong — relies on PATH
0 2 * * * backup.sh
# Correct — explicit path
0 2 * * * /opt/scripts/backup.sh
Principle of Least Privilege
Run cron jobs as the least-privileged user that can do the job:
# In /etc/crontab or /etc/cron.d/ (uses username field)
0 2 * * * www-data /opt/scripts/web-backup.sh
0 3 * * * postgres /opt/scripts/pg-backup.sh
Script Permissions
# Scripts run by root should not be writable by others
chmod 700 /opt/scripts/backup.sh
chown root:root /opt/scripts/backup.sh
# For non-root scripts
chmod 750 /opt/scripts/web-backup.sh
chown www-data:www-data /opt/scripts/web-backup.sh
Quick Reference Cheat Sheet
| Task | Schedule | Expression |
|---|---|---|
| Every minute | Continuous | * * * * * |
| Every 5 minutes | Frequent poll | */5 * * * * |
| Every 15 minutes | Quarter hour | */15 * * * * |
| Every hour | Hourly | 0 * * * * |
| Every 6 hours | 4x daily | 0 */6 * * * |
| Every day at midnight | Daily | 0 0 * * * or @daily |
| Every day at 2 AM | Nightly backup | 0 2 * * * |
| Every weekday at 9 AM | Business hours | 0 9 * * 1-5 |
| Every Sunday at 3 AM | Weekly | 0 3 * * 0 |
| First day of month | Monthly | 0 0 1 * * or @monthly |
| 1st and 15th, 2 AM | Bi-monthly | 0 2 1,15 * * |
| On system boot | At startup | @reboot |
| Once a year | Yearly | 0 0 1 1 * or @yearly |
Quick Command Reference
| Command | What It Does |
|---|---|
crontab -e |
Edit current user's crontab |
crontab -l |
List current user's crontab |
crontab -r |
Remove current user's crontab (CAREFUL) |
crontab -u USER -e |
Edit another user's crontab (as root) |
systemctl status cron |
Check if cron daemon is running |
grep CRON /var/log/syslog |
See cron execution history |
systemctl list-timers |
List all systemd timers |
flock -n /tmp/job.lock CMD |
Run CMD with exclusive lock |
Conclusion
Cron jobs are one of those tools that separate automated, well-maintained servers from manual, error-prone ones. Once you understand the syntax and the common pitfalls — PATH issues, overlapping jobs, missing output capture — scheduling tasks becomes second nature.
The core rules to remember:
- Always use absolute paths in cron commands
- Always redirect output to a log file (
>> /var/log/myjob.log 2>&1) - Use
flockfor any job that could run longer than its interval - Back up your crontab before editing (
crontab -l > ~/crontab.bak) - Run jobs as the least-privileged user possible
- Consider systemd timers when you need proper logging or dependency ordering
If you are running these jobs on a server managed by Panelica, the cron scheduler is built directly into the panel. You can create, edit, and monitor cron jobs per user from a clean UI without touching the command line — with execution logs, failure alerts, and per-user isolation out of the box.