Skip to content
elephantoo

Scheduling: cron & systemd timers

Lesson 26 of 31 14 min read

crontab syntax, environment gotchas, logging cron jobs, and systemd timers as a modern alternative.


Backups at 2 a.m., a cleanup every hour, a report every Monday morning: servers are full of jobs that must run on a schedule without anyone logged in. Linux has two tools for this: the classic cron, and systemd timers. You'll learn both, along with the gotchas that make cron jobs mysteriously fail.

cron basics#

The cron daemon wakes up every minute, checks the schedules, and runs whatever is due. Each user has a personal crontab (cron table):

Terminal
crontab -e        # edit your crontab (opens $EDITOR; nano by default on Ubuntu)
crontab -l        # list it
crontab -r        # remove it entirely (careful: no confirmation!)
sudo crontab -u www-data -l     # another user's crontab

On Ubuntu and Debian, cron is installed and running by default. On Fedora/RHEL it's called cronie: sudo dnf install cronie && sudo systemctl enable --now crond.

The crontab format#

Each line is five time fields followed by the command:

Output
┌───────────── minute        (0-59)
│ ┌─────────── hour          (0-23)
│ │ ┌───────── day of month  (1-31)
│ │ │ ┌─────── month         (1-12 or jan-dec)
│ │ │ │ ┌───── day of week   (0-7 or sun-sat; 0 and 7 are Sunday)
│ │ │ │ │
* * * * *  command to run
SyntaxMeaningExample
*every value* * * * *: every minute
Nthat value0 9 * * *: 09:00 daily
a,ba list0 9,18 * * *: 09:00 and 18:00
a-ba range0 9 * * 1-5: 09:00 on weekdays
*/nevery n*/15 * * * *: every 15 minutes

Common schedules:

Output
*/5 * * * *     every 5 minutes
0 * * * *       every hour, on the hour
30 2 * * *      02:30 every day
0 3 * * 0       03:00 every Sunday
0 0 1 * *       midnight on the 1st of each month
15 8 * * 1-5    08:15 Monday to Friday
@reboot         once at startup
@daily          once a day (same as 0 0 * * *); also @hourly, @weekly, @monthly

💡 Unsure about a schedule? Paste it into crontab.guru for a plain-English explanation.

When both day-of-month and day-of-week are set (not *), cron runs the job when either matches. 0 0 13 * 5 means "the 13th AND every Friday", not "Friday the 13th".

A real crontab#

crontab -e
# Environment for all jobs below
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""

# m  h  dom mon dow  command
30   2  *   *   *    /home/ada/bin/backup.sh >> /home/ada/logs/backup.log 2>&1
*/10 *  *   *   *    /usr/bin/flock -n /tmp/sync.lock /home/ada/bin/sync.sh
0    7  *   *   1    /home/ada/bin/weekly-report.sh | /usr/bin/logger -t weekly-report
@reboot              /home/ada/bin/on-boot.sh

Things to notice:

  • Absolute paths everywhere.
  • Output is redirected to a log file (>> file 2>&1) or sent to syslog with logger.
  • flock -n stops a new run from starting while the previous one is still going (very important for frequent jobs).
  • MAILTO="" turns off cron's attempts to email output. Set MAILTO=you@example.com if the server can send mail.

Why cron jobs fail (and how to fix them)#

1. A minimal environment. Cron doesn't read your .bashrc. PATH is usually just /usr/bin:/bin, there are no aliases, and none of your exported variables exist. Use absolute paths, set PATH in the crontab, or set what you need at the top of the script. To see exactly what cron gives you, add a temporary job:

Output
* * * * * env > /tmp/cron-env.txt

2. The working directory is $HOME. Relative paths like ./data resolve from your home directory, not from where the script lives. Inside scripts, cd explicitly. This idiom changes into the script's own directory:

Terminal
mkdir -p tools && cat > tools/where.sh <<'EOF'
#!/usr/bin/env bash
cd "$(dirname "$0")" || exit 1
echo "running in: $PWD"
EOF
chmod +x tools/where.sh
./tools/where.sh
Output
running in: /home/ada/tools

3. % characters. In a crontab line % means newline, so date +%F breaks. Escape it as \%, or (better) put the command in a script:

Output
0 1 * * * tar -czf /backups/home-$(date +\%F).tar.gz /home/ada

4. No output, so no clue. Always log. Then check the log, the journal (journalctl -u cron, or -u crond on Fedora), or grep CRON /var/log/syslog to see if the job started at all.

5. Permissions and the shebang. The script must be executable (chmod +x) with a correct #! line, or call it as /bin/bash /path/script.sh.

6. The timezone. Cron uses the system timezone (timedatectl). Servers are often on UTC, so 0 9 * * * may not be 9 a.m. where you are.

Testing a job like cron would

Run your script with an empty environment to catch PATH and variable problems before cron does:

Terminal
cat > job.sh <<'EOF'
#!/bin/bash
echo "PATH=$PATH"
echo "HOME=${HOME:-<unset>}"
date +%F >/dev/null && echo "date works"
EOF
chmod +x job.sh
env -i /bin/bash -c ./job.sh
Output
PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:.
HOME=<unset>
date works

(Here bash supplies a default PATH because the environment was empty; cron sets its own, similar minimal PATH.)

System-wide cron#

Besides personal crontabs, there are system locations (edit with sudo):

LocationNotes
/etc/crontabsystem crontab with an extra user field: 0 4 * * * root /usr/local/bin/cleanup
/etc/cron.d/drop-in files in the same format as /etc/crontab; used by packages, and handy for config management
/etc/cron.hourly/, cron.daily/, cron.weekly/, cron.monthly/executable scripts run by run-parts (via anacron on desktops). Filenames must not contain dots!

Access control: /etc/cron.allow and /etc/cron.deny decide which users may have crontabs.

systemd timers: the modern alternative#

On systemd systems, a timer unit can trigger a service unit on a schedule. Compared with cron you get logs in the journal automatically, no overlapping runs (a service that's already running isn't started again), dependency handling, resource limits, randomised delays, and catch-up of runs missed while the machine was off (Persistent=true).

Two files, a service and a timer with the same name:

/etc/systemd/system/backup.service
[Unit]
Description=Nightly backup

[Service]
Type=oneshot
User=ada
ExecStart=/home/ada/bin/backup.sh
/etc/systemd/system/backup.timer
[Unit]
Description=Run backup.service every night

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
RandomizedDelaySec=10min

[Install]
WantedBy=timers.target

Enable the timer (not the service):

Terminal
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer
sudo systemctl start backup.service     # run the job right now, to test it
journalctl -u backup.service -n 20      # its output and exit status

OnCalendar examples: hourly, daily, weekly, Mon..Fri 08:15, *-*-01 00:00:00 (monthly), *:0/15 (every 15 minutes). Check an expression with systemd-analyze calendar:

Output
$ systemd-analyze calendar "Mon..Fri 08:15"
  Original form: Mon..Fri 08:15
Normalized form: Mon..Fri *-*-* 08:15:00
    Next elapse: Fri 2026-10-02 08:15:00 IST
       (in UTC): Fri 2026-10-02 02:45:00 UTC
       From now: 21h left

There are relative timers too: OnBootSec=5min (5 minutes after boot), OnUnitActiveSec=1h (an hour after the last run).

cron or timer?

Use cron whenUse a systemd timer when
you want a quick one-linerthe job matters in production
the system may not use systemd (containers, BSD, macOS)you want journald logging and systemctl status
you're editing a user's personal scheduleruns must not overlap, or missed runs should catch up

One-off jobs: at#

To run something once later, use at (sudo apt install at):

Terminal
echo "/home/ada/bin/deploy.sh" | at 23:00
at now + 30 minutes <<< "systemctl restart myapp"
atq          # list pending jobs
atrm 3       # remove job 3

For a one-off with systemd: systemd-run --on-active=30m /home/ada/bin/task.sh.

Common mistakes#

  • Relying on your interactive environment (PATH, aliases, variables) inside cron jobs.
  • Not logging output, so failures are invisible.
  • Unescaped % in crontab lines.
  • Overlapping runs of a slow job scheduled every few minutes. Use flock or a systemd timer.
  • Forgetting the timezone: servers are often on UTC.
  • crontab -r instead of crontab -e. The keys are next to each other, and -r deletes everything without asking. Keep a copy of important crontabs (crontab -l > crontab.bak).
  • Enabling the .service instead of the .timer with systemd.

What's next#

Many scheduled jobs talk to other machines. Next, start the final module with networking commands: IP addresses, ports, DNS, and how to troubleshoot connectivity.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What schedule does 30 2 * * 1-5 mean?

  2. 2.A script works when you run it by hand but fails from cron with 'command not found'. The most likely cause?

  3. 3.In a crontab line, why must date +%F be written as date +\%F?

Finished reading?

Mark this lesson complete to track your progress.