Crontab Syntax Review
The five time fields are minute (0-59), hour (0-23), day-of-month (1-31), month (1-12), and day-of-week (0-7, where both 0 and 7 are Sunday). A sixth field is the command. Use `*` for 'every', `/n` for 'every nth', and comma-separated values for lists.
Edit the current user's crontab with `crontab -e`. List it with `crontab -l`. Edit another user's crontab as root with `crontab -u www-data -e`. System-wide jobs go in `/etc/cron.d/` with an explicit user field inserted between the time fields and the command.
One thing that bites people: cron does not source `~/.bashrc` or `~/.profile`. It runs with a minimal environment. `PATH` is typically `/usr/bin:/bin`. Set `PATH` explicitly at the top of every crontab, or use absolute paths in every command.
# View crontab for www-data
crontab -u www-data -l
# Edit system cron job file
vim /etc/cron.d/myapp
# Cron environment check - run this to see what cron sees
* * * * * env > /tmp/cronenv.txt
Setting Environment Variables in Crontab
Without proper environment setup, scripts that work interactively fail silently in cron. Set `SHELL`, `PATH`, and `MAILTO` at the top of every user crontab. `MAILTO=''` suppresses email output entirely - useful once your logging is solid. `MAILTO=ops@example.com` routes all stdout/stderr from every job to that address.
For secrets, do not embed them in the crontab. Source an env file inside the script, or use a wrapper that calls `systemd-run` with `--property=EnvironmentFile=`. For containers and Kubernetes, cron is increasingly replaced by CronJob objects, but on bare metal and VMs, the patterns below still apply.
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=ops@example.com
HOME=/root
# Now your jobs
0 2 * * * /opt/scripts/backup.sh
Backup Jobs
A daily database dump is the most common cron job on any production server. The pattern below dumps a PostgreSQL database at 01:30, compresses it with gzip, and names the file with a datestamp. We use `--no-password` with a `.pgpass` file rather than embedding credentials.
For MySQL/MariaDB, use `--defaults-extra-file` pointing to a `[client]` section in a locked-down config file. Never use `-p` directly in crontab - `crontab -l` output can appear in logs.
The `find` command at the end prunes backups older than 14 days. Run this as the `postgres` user or a dedicated backup user, not root.
# PostgreSQL daily backup - runs as postgres user
30 1 * * * pg_dump -U app_user mydb | gzip > /var/backups/db/mydb_$(date +\%Y\%m\%d).sql.gz
# Prune backups older than 14 days
45 1 * * * find /var/backups/db/ -name '*.sql.gz' -mtime +14 -delete
# MySQL backup with credentials file
30 1 * * * mysqldump --defaults-extra-file=/etc/mysql/backup.cnf mydb | gzip > /var/backups/mysql_$(date +\%Y\%m\%d).sql.gz
Log Rotation and Cleanup
logrotate handles most log rotation, but custom application logs often need direct cron management. The pattern below compresses logs older than 1 day and deletes anything older than 30 days. We tested this on servers generating 50GB/day of application logs - the `find` + `gzip` pipeline keeps disk usage predictable without touching logrotate config.
For high-volume systems, consider running cleanup jobs during off-peak windows and verifying with `df -h` before and after. Add a disk usage alert before the cleanup so you know the baseline.
# Compress yesterday's logs
5 0 * * * find /var/log/myapp/ -name '*.log' -mtime +1 -not -name '*.gz' -exec gzip {} \;
# Delete logs older than 30 days
10 0 * * * find /var/log/myapp/ -name '*.log.gz' -mtime +30 -delete
# Check disk usage and email if over 80%
*/15 * * * * df -h / | awk 'NR==2 {gsub("%","",$5); if($5>80) print "DISK ALERT: " $5"% used on /"}' | mail -s 'Disk Alert' ops@example.com
Preventing Overlapping Jobs with Flock
`flock` is the correct tool for preventing a slow cron job from launching a second instance before the first finishes. Without it, a job that occasionally runs long will stack up instances, exhaust resources, and cause cascading failures. We have seen this take down database servers.
The `-n` flag makes flock non-blocking: if the lock is held, the new invocation exits immediately instead of waiting. Use `-n` for jobs where skipping is acceptable. Use blocking mode (drop `-n`) only when you want the second instance to queue.
Check `/proc/$(cat /var/run/myjob.lock)/status` if you need to verify which PID holds the lock.
# Non-blocking: skip if already running
*/5 * * * * /usr/bin/flock -n /var/lock/myjob.lock /opt/scripts/myjob.sh
# With timeout: wait up to 30 seconds, then give up
*/5 * * * * /usr/bin/flock -w 30 /var/lock/myjob.lock /opt/scripts/myjob.sh
# Inline lock without a wrapper script
*/10 * * * * ( flock -n 9 || exit 1; /opt/scripts/heavyjob.sh ) 9>/var/lock/heavyjob.lock
HTTP Health Checks and Monitoring
curl-based health checks from cron are a lightweight complement to external monitoring. They catch issues that uptime monitors miss when your monitoring SaaS itself has a problem. We run these every minute on critical services and pipe failures to PagerDuty via the API or a simple mail relay.
The `--max-time` flag is critical - without it, a hung connection blocks the cron slot indefinitely. Set it lower than your cron interval. `--silent --fail` ensures curl exits non-zero on HTTP 4xx/5xx, which triggers the `||` branch.
# HTTP health check every minute
* * * * * /usr/bin/curl --silent --fail --max-time 10 https://app.example.com/health || echo "app.example.com health check failed" | mail -s 'Health Alert' ops@example.com
# Check SSL cert expiry - alert if expiring within 30 days
0 8 * * 1 /usr/bin/curl --silent https://app.example.com -o /dev/null --cert-status 2>&1 | grep -i 'expire' | mail -s 'SSL Cert Check' ops@example.com
# Log response time for trending
*/5 * * * * /usr/bin/curl --silent --output /dev/null --write-out '%{time_total}\n' https://app.example.com >> /var/log/response_time.log
System Maintenance Jobs
Package updates, cache clearing, and temp file cleanup are routine but need careful scheduling. On our test server running Ubuntu 24.04, `unattended-upgrades` handles security patches, but we still cron a weekly `apt autoremove` to reclaim disk space from old kernel packages. This is safe to automate; confirm with `--dry-run` first.
For clearing systemd journal logs, `journalctl --vacuum-size=500M` keeps the journal under 500MB. Run it weekly. On busy systems running with default journal settings, we have seen journals grow to 8GB inside two months.
Temp directory cleanup using `systemd-tmpfiles` is preferred on systemd systems, but a cron fallback is useful on older setups.
# Weekly apt cleanup - Sunday at 03:00
0 3 * * 0 /usr/bin/apt-get autoremove -y >> /var/log/apt-autoremove.log 2>&1
# Journal vacuum - keep under 500MB, run weekly
0 4 * * 0 /usr/bin/journalctl --vacuum-size=500M
# Clear files in /tmp older than 7 days
0 5 * * * /usr/bin/find /tmp -type f -atime +7 -delete
# Sync system clock - useful if chrony/ntpd is unreliable
30 * * * * /usr/sbin/ntpdate -s pool.ntp.org
Advanced Scheduling Patterns
Run a job on the last day of the month by combining day-of-month and a shell test inside the command. Cron has no native 'last day' field, so check with `date`.
Run jobs only on weekdays using the day-of-week field: `1-5` covers Monday through Friday. Run jobs every 6 hours starting at 06:00: `0 6,12,18,0 * * *` or use step syntax `0 */6 * * *` - note that `*/6` starts at 00:00, not 06:00.
For DevOps teams building complex pipeline automation beyond what cron handles cleanly - dependency chaining, retries, dashboards - tools like taskbotshub.ai provide structured task orchestration that complements rather than replaces cron for simpler jobs.
Run a job four times an hour at specific minutes using comma separation: `0,15,30,45 * * * *`. This is clearer than `*/15` when you need to be explicit about start alignment.
# Run on the last day of the month
59 23 28-31 * * [ "$(date -d tomorrow +\%d)" = '01' ] && /opt/scripts/month_end.sh
# Weekdays only at 08:00
0 8 * * 1-5 /opt/scripts/business_report.sh
# Every 6 hours exactly on the hour
0 0,6,12,18 * * * /opt/scripts/sync.sh
# First Monday of the month
0 9 1-7 * 1 /opt/scripts/weekly_first.sh
Output Handling and Logging
Every cron job should have explicit output handling. Unhandled stdout goes to the `MAILTO` address, which floods inboxes and gets ignored. Redirect stdout and stderr to a log file with rotation, or use `logger` to send output to syslog where it integrates with your existing log pipeline.
Use `>> /var/log/myjob.log 2>&1` to append both streams. Rotate that log with logrotate or a separate cron job. For jobs where you only want to hear about failures, use the `chronic` utility from the `moreutils` package - it suppresses output unless the command exits non-zero.
On our infrastructure, we standardize on `logger -t myjob` for cron output. This lets us grep `/var/log/syslog` or use `journalctl -t myjob` without managing separate log files per job.
# Append all output to log file
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# Send to syslog via logger
0 3 * * * /opt/scripts/cleanup.sh 2>&1 | /usr/bin/logger -t cleanup
# Suppress output unless failure (requires moreutils)
0 4 * * * /usr/bin/chronic /opt/scripts/maintenance.sh
# Install moreutils if needed
# apt-get install moreutils
System Cron Directories vs User Crontab
Know the difference between user crontabs (`/var/spool/cron/crontabs/`), the system crontab (`/etc/crontab`), and the drop-in directory `/etc/cron.d/`. Files in `/etc/cron.d/` require an explicit username field after the time fields - skip it and the job silently fails on most distros.
Frequency shortcuts `@reboot`, `@daily`, `@hourly`, `@weekly`, and `@monthly` work in user crontabs and `/etc/cron.d/` files. `@reboot` is particularly useful for starting services or scripts after unexpected reboots without duplicating systemd unit logic.
For scripts that should run at boot and also on a schedule, combine `@reboot` with a time-based rule. On our test server, we use `@reboot` to warm application caches after a restart, which otherwise take 10-15 minutes to populate naturally.
# /etc/cron.d/myapp - note the username field
PATH=/usr/local/bin:/usr/bin:/bin
# Format: min hour dom month dow USER command
0 2 * * * root /opt/myapp/backup.sh
30 1 * * * www-data /opt/myapp/cleanup.sh
# In user crontab - no username field
@reboot /opt/scripts/warm_cache.sh
@daily /opt/scripts/daily_report.sh