Cron is the standard Unix task scheduler — it runs commands at specified intervals automatically, even when no user is logged in. Whether you're clearing temporary files, running database backups, sending scheduled emails, or renewing SSL certificates, cron is how you automate recurring server tasks. This guide covers everything from the basics to production-ready patterns.
What Is Cron?
Cron is a daemon (background service) that runs continuously on Linux systems. It reads a list of scheduled tasks from crontab files and executes them at the specified times. Think of it as a persistent alarm clock for your server — you set it once and it fires reliably without any manual intervention.
# Check if cron is running
sudo systemctl status cron # Ubuntu/Debian
sudo systemctl status crond # CentOS/RHEL
# Start and enable cron
sudo systemctl start cron
sudo systemctl enable cron
The Crontab File
Scheduled tasks are stored in a crontab (cron table) file. Each user has their own crontab, plus there are system-wide crontab files.
crontab -e # open your personal crontab for editing
crontab -l # list your current cron jobs
crontab -r # remove all your cron jobs (careful!)
crontab -u www-data -e # edit another user's crontab (requires root)
# System-wide crontab files (have an extra 'user' field)
/etc/crontab
/etc/cron.d/your-app
Cron Expression Syntax
Each cron job line has 5 time fields followed by the command to run:
┌──────────── minute (0–59)
│ ┌─────────── hour (0–23)
│ │ ┌────────── day of month (1–31)
│ │ │ ┌───────── month (1–12)
│ │ │ │ ┌──────── day of week (0–7, 0 and 7 are Sunday)
│ │ │ │ │
* * * * * command to run
Field Values
* # any value (every minute, every hour, etc.)
*/15 # every 15 units (every 15 minutes, every 2 hours with */2)
0-5 # range (minutes 0 through 5)
1,3,5 # list (months January, March, May)
1-5/2 # range with step (minutes 1, 3, 5)
Common Cron Schedule Examples
# Every minute
* * * * * /path/to/script.sh
# Every 5 minutes
*/5 * * * * /path/to/script.sh
# Every hour (at :00)
0 * * * * /path/to/script.sh
# Every day at midnight
0 0 * * * /path/to/script.sh
# Every day at 9:30 AM
30 9 * * * /path/to/script.sh
# Every Monday at 9 AM
0 9 * * 1 /path/to/script.sh
# Every weekday (Mon–Fri) at 8 AM
0 8 * * 1-5 /path/to/script.sh
# Every first day of the month at midnight
0 0 1 * * /path/to/script.sh
# Every Sunday at 2 AM (good for weekly backups)
0 2 * * 0 /path/to/backup.sh
# Twice a day (noon and midnight)
0 0,12 * * * /path/to/script.sh
# Every 15 minutes during business hours (9 AM–6 PM) on weekdays
*/15 9-18 * * 1-5 /path/to/script.sh
# Every 6 hours
0 */6 * * * /path/to/script.sh
Special Shortcuts
@reboot # run once at startup
@hourly # equivalent to 0 * * * *
@daily # equivalent to 0 0 * * *
@weekly # equivalent to 0 0 * * 0
@monthly # equivalent to 0 0 1 * *
@yearly # equivalent to 0 0 1 1 *
# Example
@daily /var/scripts/clear-old-logs.sh
@reboot /var/scripts/start-queue-worker.sh
Real-World Cron Job Examples
# Database backup at 2 AM daily
0 2 * * * pg_dump -U postgres myapp > /backups/myapp_$(date +\%Y\%m\%d).sql
# Clear temp uploads older than 24 hours, every hour
0 * * * * find /var/www/tmp -type f -mtime +1 -delete
# Renew SSL certificate (Certbot does this automatically, but manual example)
0 3 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
# Run a PHP artisan command (Laravel)
* * * * * cd /var/www/myapp && php artisan schedule:run >> /dev/null 2>&1
# Node.js script via PM2
0 4 * * * /usr/bin/node /var/scripts/send-daily-digest.js
# Pull latest Docker image and restart
0 5 * * 0 docker pull myapp:latest && docker compose up -d
# Clear Redis cache at midnight
0 0 * * * redis-cli FLUSHDB
# Generate sitemap daily
0 1 * * * curl -s https://yoursite.com/api/generate-sitemap > /dev/null
Environment Variables in Cron
Cron runs with a minimal environment — it doesn't load your ~/.bashrc, ~/.profile, or any environment variables you set in your shell session. This causes the most common cron failures.
# The cron environment has:
# HOME=/root (or user's home)
# LOGNAME=username
# PATH=/usr/bin:/bin (very limited — not /usr/local/bin or /home/user/.nvm)
# SHELL=/bin/sh (not bash!)
# Solution 1: Set variables at the top of crontab
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin:/home/user/.nvm/versions/node/v22.0.0/bin
HOME=/home/user
# Solution 2: Use absolute paths in commands
0 * * * * /home/user/.nvm/versions/node/v22.0.0/bin/node /var/scripts/task.js
# Solution 3: Source your profile in the command
0 * * * * source /home/user/.bashrc && node /var/scripts/task.js
# Solution 4: Use a wrapper script
0 * * * * /var/scripts/run-with-env.sh
A wrapper script approach is the cleanest for complex scripts:
#!/bin/bash
# /var/scripts/run-with-env.sh
set -e
export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh"
source /var/www/myapp/.env
cd /var/www/myapp
node scripts/daily-digest.js
Logging Cron Output
By default, cron sends output to the user's local mail. Usually you want to redirect to a log file instead:
# Redirect both stdout and stderr to a log file (appending)
0 2 * * * /path/to/script.sh >> /var/log/myapp/backup.log 2>&1
# Discard all output (silent execution)
0 2 * * * /path/to/script.sh > /dev/null 2>&1
# Write to a rotating log (use with logrotate)
0 2 * * * /path/to/script.sh >> /var/log/myapp/backup.log 2>&1
# Timestamp each log entry
0 2 * * * echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] Starting backup" >> /var/log/myapp/backup.log; /path/to/backup.sh >> /var/log/myapp/backup.log 2>&1
Logrotate Configuration
Prevent log files from growing forever:
# /etc/logrotate.d/myapp-cron
/var/log/myapp/*.log {
daily
rotate 14 # keep 14 days
compress
missingok
notifempty
create 0640 www-data www-data
}
System-Wide Crontab (/etc/cron.d/)
Files in /etc/cron.d/ are system-wide crontabs — they require a username field after the time fields:
# /etc/cron.d/myapp
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
# minute hour dom month dow user command
0 2 * * * www-data /var/www/myapp/scripts/backup.sh >> /var/log/myapp/backup.log 2>&1
*/5 * * * * www-data /var/www/myapp/scripts/health-check.sh > /dev/null 2>&1
This is the preferred approach for application cron jobs — it keeps them separate from user crontabs, survives user account changes, and is easy to deploy via configuration management tools.
Debugging Failed Cron Jobs
# Check cron logs
sudo tail -f /var/log/syslog | grep cron # Ubuntu/Debian
sudo tail -f /var/log/cron # CentOS/RHEL
# Run the command manually as the cron user to test
sudo -u www-data /path/to/your/script.sh
# Simulate cron environment
env -i HOME=/home/user LOGNAME=user PATH=/usr/bin:/bin /bin/sh -c '/path/to/script.sh'
# Check the script's exit code
echo $? # 0 = success, non-zero = error
# Verify crontab syntax
crontab -l # list current jobs — syntax errors prevent the file from being saved
Common Failure Reasons
- Script not executable — fix:
chmod +x /path/to/script.sh - Command not found — cause: cron's minimal PATH; fix: use absolute paths
- Environment variables missing — cause: cron doesn't load your shell profile
- Working directory wrong — fix: add
cd /var/www/myapp &&before the command - Percent signs in command —
%has special meaning in crontab (newline); escape as\%or use a script file
Alternatives to Cron
Systemd Timers (modern Linux)
# /etc/systemd/system/backup.service
[Unit]
Description=Database Backup
[Service]
Type=oneshot
User=www-data
ExecStart=/var/scripts/backup.sh
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 2 AM
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true # run if missed while system was off
[Install]
WantedBy=timers.target
sudo systemctl enable --now backup.timer
sudo systemctl list-timers # see all timers and when they'll next run
Systemd timers have better logging (via journalctl), dependency management, and can be triggered by events (not just time). For new services on systemd-based systems, timers are the preferred approach.
Application-Level Schedulers
- Laravel:
php artisan schedule:runvia one cron entry; define all schedules inapp/Console/Kernel.phpin PHP - Node.js:
node-cronoragenda— schedule tasks within the Node.js process - BullMQ (Node.js/Redis): job queue with repeatable jobs, retry logic, and job history
- Inngest: managed background jobs and cron with replay, retries, and monitoring as a SaaS
Summary
Cron is reliable, universal, and zero-overhead for simple scheduled tasks. The cron expression */15 9-17 * * 1-5 means "every 15 minutes, 9 AM to 5 PM, weekdays" — once you understand the 5-field syntax, you can schedule almost anything. The main gotchas: always use absolute paths, set your PATH explicitly, redirect output to log files, and test commands manually before adding to crontab. For more complex scheduling needs — dependencies between jobs, retry logic, monitoring — application-level schedulers like BullMQ or managed services like Inngest are worth the additional setup.
CREESOL