Both mechanisms solve the same problem - run this command at this time, repeatedly, without a human triggering it - but differ in how much infrastructure they need and what extra features come with them.

Cron

Each user can have their own crontab (cron table):

crontab -e      # edit your own crontab
crontab -l      # list it
sudo crontab -e -u www-data   # edit another user's, as root

Each line has five time fields, then the command:

# minute  hour  day-of-month  month  day-of-week  command
30        2     *             *      *            /usr/local/bin/backup.sh
0         */4   *             *      *            /usr/local/bin/check-disk.sh
0         9     *             *      1-5          /usr/local/bin/weekday-report.sh
Field Range * means
minute 0–59 every minute
hour 0–23 every hour
day of month 1–31 every day
month 1–12 every month
day of week 0–7 (0 and 7 both = Sunday) every day of the week

*/4 in the hour field means "every 4 hours"; 1-5 in the day-of-week field means Monday through Friday. Test unfamiliar syntax at crontab.guru before deploying it - a single wrong field is easy to miss and can run something far more or less often than intended.

System-wide cron locations

Beyond individual users' crontabs:

/etc/crontab          # system crontab - includes a username field crontab -e doesn't have
/etc/cron.d/*         # drop-in system crontabs, same format as /etc/crontab
/etc/cron.daily/*     # scripts run once a day (no schedule needed - just drop a script in)
/etc/cron.hourly/*
/etc/cron.weekly/*
/etc/cron.monthly/*

The cron.daily/hourly/etc. directories are the simplest option for "run this script periodically" when the exact time doesn't matter - no crontab syntax needed, just an executable script in the right directory.

The classic gotcha: cron's environment is minimal

A script that works fine run manually can fail silently under cron, almost always because cron jobs run with a much sparser $PATH and no inherited shell environment - see Environment Variables. Fixes:

# in the script itself
export PATH=/usr/local/bin:/usr/bin:/bin

# or, use absolute paths throughout
0 2 * * * /usr/bin/python3 /home/alice/scripts/backup.py

Always redirect a cron job's output somewhere you'll actually see it - cron mails output to the crontab's owner by default, which is easy to miss if local mail delivery isn't configured at all:

0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

systemd timers: the modern alternative

A timer unit pairs with a service unit (see systemd and Services) to schedule it:

# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true

[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Service]
ExecStart=/usr/local/bin/backup.sh
sudo systemctl enable --now backup.timer
systemctl list-timers

Persistent=true catches up a missed run if the machine was off at the scheduled time - something cron has no equivalent for. Timer output goes to journald automatically (see Logs and journald), no manual redirection needed, and timers can express dependencies on other units, which a plain cron line can't.

Which to use

Cron remains simpler for a quick, single-purpose schedule and is universally available regardless of init system. systemd timers are worth the extra setup when you want dependency ordering, automatic logging, or catch-up-on-missed-runs behavior - increasingly the default choice on systemd-based distributions for anything beyond a trivial script.