Tutorial

Cron Jobs & Scheduled Tasks: The Complete Linux Guide

Back to Blog
A modern alternative to cPanel, Plesk and CyberPanel — isolated, secure, AI-assisted.
Start free

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

  1. The cron daemon starts at boot and runs continuously in the background.
  2. Every minute, it reads all crontab files from the spool directory and the system crontab.
  3. It compares the current time against every job definition.
  4. If the current time matches the schedule expression, it forks a child process and runs the command.
  5. 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 -r and crontab -e are 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/bash if 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 .shrun-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 flock for 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.

Security-first hosting panel

Run your servers on a modern panel.

Panelica is a modern, security-first hosting panel — isolated services, built-in Docker and AI-assisted management, with one-click migration from any panel.

Zero-downtime migration Fully isolated services Cancel anytime
Share:
Built in Go.