Cron runs commands on a schedule: backups at 2 a.m., a cleanup every hour, a report every Monday. It’s on every Linux server and has barely changed in decades. Most cron problems come from a few well-known pitfalls, which this guide covers alongside the basics.
Edit your crontab
Each user has a personal crontab:
crontab -e # edit (choose an editor the first time)
crontab -l # list
crontab -r # remove ALL your jobs — no confirmation!
sudo crontab -u www-data -e # edit another user's crontab
crontab -r sits right next to -e on the keyboard. Back up with crontab -l > ~/crontab.bak before experimenting.
The syntax
┌───────── minute (0–59)
│ ┌─────── hour (0–23)
│ │ ┌───── day of month (1–31)
│ │ │ ┌─── month (1–12)
│ │ │ │ ┌─ day of week (0–7, 0 and 7 = Sunday)
│ │ │ │ │
* * * * * command
30 2 * * * /usr/local/bin/backup.sh # every day at 02:30
*/15 * * * * /usr/local/bin/check.sh # every 15 minutes
0 * * * * /usr/local/bin/hourly.sh # every hour, on the hour
0 9 * * 1-5 /usr/local/bin/report.sh # 09:00 Monday–Friday
0 0 1 * * /usr/local/bin/monthly.sh # midnight on the 1st
@reboot /usr/local/bin/on-boot.sh # once at startup
Shortcuts: @hourly, @daily, @weekly, @monthly, @yearly, @reboot. Many more patterns in cron expression examples.
Gotcha: if you set both day-of-month and day-of-week, cron runs the job when either matches. 0 0 1 * 1 runs on the 1st and every Monday.
Times are in the server’s time zone
Cron uses the system time zone. Check it with timedatectl. Many VPS images default to UTC — “every day at 9” then means 09:00 UTC. Either convert, or set the zone (see timedatectl and time zones) and restart cron.
System-wide cron
Besides user crontabs:
/etc/crontaband files in/etc/cron.d/— these have an extra user field:
# /etc/cron.d/myapp
SHELL=/bin/bash
30 3 * * * deploy /home/deploy/app/bin/cleanup >> /var/log/myapp-cleanup.log 2>&1
/etc/cron.hourly/,cron.daily/,cron.weekly/,cron.monthly/— drop an executable script in, no schedule needed. File names must not contain dots (backup.shis silently ignored byrun-partson Debian/Ubuntu; name itbackup).
Files in /etc/cron.d/ are good for packaging jobs with an application and tracking them in configuration management.
Pitfall 1: a minimal environment
Cron runs jobs with a tiny environment — typically PATH=/usr/bin:/bin, no .bashrc, shell /bin/sh. A script that works in your terminal fails in cron because node, php or docker aren’t found.
Fixes:
# Set variables at the top of the crontab
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# Or use absolute paths
*/5 * * * * /usr/bin/php /var/www/app/artisan schedule:run
Find the absolute path with command -v php. Tools installed per user (nvm, pyenv) need their init script sourced explicitly. More in cron environment variables.
Pitfall 2: % is special
In a crontab line, % means newline. Escape it:
0 1 * * * tar czf /backups/www-$(date +\%F).tar.gz /var/www
Or move the command into a script, which avoids the problem entirely.
Pitfall 3: output goes nowhere (or to mail)
Cron emails a job’s output to the user if a mail system is configured; otherwise it’s dropped. Log it yourself:
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
2>&1 sends errors to the same file. To discard everything: > /dev/null 2>&1 — but then you won’t know when it fails. Set [email protected] at the top of the crontab if the server can send mail.
Prevent overlapping runs
If a job takes longer than its interval, copies pile up. flock ensures only one runs:
*/5 * * * * flock -n /tmp/sync.lock /usr/local/bin/sync.sh
-n makes a second copy exit immediately instead of waiting.
Is cron running my job?
systemctl status cron # Debian/Ubuntu ("crond" on RHEL family)
grep CRON /var/log/syslog # Debian/Ubuntu
journalctl -u cron --since today
The log shows CMD (...) each time a job starts. If it starts but doesn’t work, the problem is the command — run it with cron’s environment to reproduce:
env -i SHELL=/bin/sh PATH=/usr/bin:/bin HOME=$HOME /bin/sh -c '/usr/local/bin/backup.sh'
Checklist when a job doesn’t run: syntax (use a checker), missing newline at the end of the crontab, script not executable (chmod +x), wrong user’s crontab, % unescaped, dots in /etc/cron.daily file names. More in cron not running.
Monitoring cron jobs
A silent failure is worse than a loud one. Have important jobs ping a monitor on success:
30 2 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 https://hc-ping.com/YOUR-UUID > /dev/null
If the ping doesn’t arrive, Healthchecks.io (or a self-hosted equivalent) alerts you.
Cron or systemd timers?
systemd timers offer logging via journald, dependencies, randomized delays and catch-up after downtime (Persistent=true). Cron is simpler and universal. For new, important jobs on a modern distribution, timers are worth a look — see systemd timers. For quick scheduled tasks, cron is perfect.
A VPS runs around the clock, which makes it the natural home for scheduled jobs — backups, scrapers, reports, bots. Every VPS.DO plan includes full root access to cron and systemd.