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/crontab and 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.sh is silently ignored by run-parts on Debian/Ubuntu; name it backup).

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.

  • Linux
  • Cron
  • Automation