Cron Jobs Explained: Scheduling Tasks on Linux (Complete Guide 2026)

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:run via one cron entry; define all schedules in app/Console/Kernel.php in PHP
  • Node.js: node-cron or agenda — 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.

Frequently Asked Questions

What's the minimum interval for cron? Can I run a job every second?

Cron's minimum interval is one minute. You cannot schedule a job for every second using cron. For sub-minute scheduling, you need an application-level solution: a Node.js setInterval, a systemd timer with OnUnitActiveSec=10s, or a worker that loops and sleeps. If you genuinely need per-second scheduling, the overhead of cron (forking a new process every second) would be significant anyway — an in-process scheduler is more appropriate.

Will a cron job run if my server restarts?

Yes — cron starts automatically with the server (if enabled via systemctl enable cron) and will run scheduled jobs at their next due time. If the server was off when a job was supposed to run, standard cron will not catch up and run the missed job. Use @reboot for jobs that should run on each startup. For jobs where "catch up on missed runs" is important (e.g., a billing job), use Persistent=true with a systemd timer — it runs the job once on startup if it was missed while the system was off.

How do I prevent a cron job from running if the previous run is still active?

Use flock to create a lock file: 0 * * * * /usr/bin/flock -n /tmp/myjob.lock /path/to/script.sh. The -n flag means "fail immediately if lock is held" — the new instance exits instead of waiting. This prevents overlapping runs. For more complex job queue management (retries, monitoring, dependencies), use a proper job queue like BullMQ (Node.js/Redis) rather than trying to manage concurrency via shell scripts.

Can I use cron to send scheduled emails?

Yes, but implement it at the application level rather than using cron to call a mail command directly. The recommended pattern: a cron job runs a script every minute (or every hour, depending on volume), the script queries the database for emails due to be sent (where send_at <= NOW() and sent = false), sends them via your SMTP service (SendGrid, Resend, SES), and marks them as sent. This approach is reliable, retryable, and keeps email logic in your application code where it's testable — not buried in a shell script.