System administrators and developers frequently need recurring operations to happen automatically: nightly database dumps, hourly log rotations, SSL certificate renewals, and cache flushing. In Linux, scheduled recurring background tasks are managed by the cron daemon (cron / crond).
The configuration tables that define scheduled tasks are called crontabs. Every minute, the cron daemon wakes up, scans all active crontab files, and executes any commands matching the current system time.
Each line in a user crontab consists of 5 time-specification fields followed by the absolute command string to execute:
┌───────────── Minute (0 - 59)
│ ┌───────────── Hour (0 - 23 in 24-hr format)
│ │ ┌───────────── Day of the Month (1 - 31)
│ │ │ ┌───────────── Month of the Year (1 - 12 or Jan - Dec)
│ │ │ │ ┌───────────── Day of the Week (0 - 7, where 0 and 7 = Sun)
│ │ │ │ │
* * * * * <command to execute>
| Operator | Meaning | Example Usage |
|---|---|---|
Asterisk (*) |
Matches every possible value in that field. | * * * * * (Runs every single minute). |
Comma (,) |
Specifies an explicit list of discrete values. | 0 9,13,17 * * * (Runs at 9 AM, 1 PM, and 5 PM). |
Hyphen (-) |
Defines an inclusive range of numbers. | 0 9-17 * * 1-5 (Runs hourly from 9 AM to 5 PM, Mon through Fri). |
Slash (/) |
Specifies step/interval increments. | */15 * * * * (Runs every 15 minutes). |
Never edit user crontab spool files in /var/spool/cron/crontabs/ directly! Always use the official crontab utility:
# Edit active crontab for current user (opens $EDITOR, e.g. nano or vi)
$ crontab -e
# View all active scheduled jobs for current user
$ crontab -l
# Remove active crontab completely (WARNING: no undo!)
$ crontab -r
# Root administrator viewing another user's crontab
$ sudo crontab -u alex -l
Systemd and modern cron implementations support convenient predefined macro strings that replace the five asterisk fields:
| Macro String | Standard Equivalent | Execution Schedule |
|---|---|---|
@reboot |
None | Executes once immediately after system bootup. |
@hourly |
0 * * * * |
Executes at the beginning of every hour (Minute 0). |
@daily |
0 0 * * * |
Executes every midnight (00:00). |
@weekly |
0 0 * * 0 |
Executes every Sunday at midnight. |
@monthly |
0 0 1 * * |
Executes on the 1st day of every month at midnight. |
The #1 reason beginner shell scripts fail under cron is that cron executes with an extremely stripped-down $PATH (often only /usr/bin:/bin). Always follow these three golden rules:
1. Define PATH explicitly at the top of your crontab:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
2. Use absolute paths for all commands and scripts:
Use /usr/bin/python3 /home/alex/backup.py instead of python3 backup.py.
3. Always redirect standard output and standard error:
If you do not redirect output, cron attempts to send an internal system email via postfix/sendmail, silently losing errors. Redirect output and errors to a log file:
# Append both standard output (stdout) and standard error (stderr) to a log file:
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# Suppress all output completely (discard to /dev/null):
*/10 * * * * /usr/bin/curl -s https://example.com/api/ping > /dev/null 2>&1
Here is a production-grade Bash backup script that creates a compressed tarball of website files and database dumps with automatic timestamping and 7-day log retention:
#!/bin/bash
# /opt/scripts/daily_backup.sh
set -euo pipefail
BACKUP_DIR="/var/backups/web"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
TAR_FILE="$BACKUP_DIR/site_backup_$TIMESTAMP.tar.gz"
mkdir -p "$BACKUP_DIR"
# 1. Create compressed archive of web directory
/usr/bin/tar -czf "$TAR_FILE" /var/www/html/
# 2. Dump MySQL database
/usr/bin/mysqldump -u root -p'SecretPass123' app_db | /usr/bin/gzip > "$BACKUP_DIR/db_$TIMESTAMP.sql.gz"
# 3. Purge backups older than 7 days to conserve disk space
/usr/bin/find "$BACKUP_DIR" -type f -name "*.gz" -mtime +7 -delete
echo "[$(date)] Backup completed successfully: $TAR_FILE"
Schedule this script to execute every single night at 2:30 AM via crontab -e:
30 2 * * * /bin/bash /opt/scripts/daily_backup.sh >> /var/log/daily_backup.log 2>&1
# Task 1: Check if the cron service daemon is currently active
$ sudo systemctl status cron || sudo systemctl status crond
# Task 2: List system-wide cron directories in /etc
$ ls -ld /etc/cron.*
# Task 3: Schedule a one-time task 2 minutes from now using 'at'
$ echo "echo 'Testing one-time job' >> /tmp/at_test.txt" | at now + 2 minutes
$ atq # view queue of scheduled at jobs
# Task 4: View recent cron job executions in system logs
$ sudo grep -i cron /var/log/syslog || sudo journalctl -u cron -n 20
# Task 5: View the system-wide /etc/crontab file
$ cat /etc/crontab
Use the following crontab syntax:
*/10 9-18 * * 1-5 /path/to/script.sh
Here, */10 specifies every 10 minutes, 9-18 spans hours 9 through 18, and 1-5 denotes Monday through Friday.
On Debian and Ubuntu systems, individual user crontabs are stored in /var/spool/cron/crontabs/<username>. On Red Hat and CentOS systems, they reside in /var/spool/cron/<username>. These directories are restricted and accessible only by root and the crontab setuid binary.
Linux utilizes two access control files: /etc/cron.allow and /etc/cron.deny. If cron.allow exists, only usernames listed in it may use crontab. If only cron.deny exists, everyone can create crontabs except users listed inside it.
Harden your servers against external cyber threats in Class 52: Linux Firewall & Security Hardening (UFW, Firewalld & Fail2ban).
Continue to Class 52: Security & Firewall → Previous: ← Class 50: SSH Remote Access | Full Syllabus