A cron job is a command that the Linux cron service runs automatically on a recurring schedule: a backup every night at 2 a.m., clearing temp files every Sunday, or running the Laravel scheduler every minute. To create one, run crontab -e and add a line made of five time fields (minute, hour, day of month, month, day of week) followed by the command. For example, 30 2 * * * /usr/local/bin/backup.sh means every day at 2:30 a.m.

That one line is enough to get started, but most cron headaches live elsewhere: a script that works in your terminal but not under cron, output that vanishes without a trace, or two runs that overlap. This article walks through the schedule syntax with a table of examples, then those pitfalls, and finally systemd timers, which are a better fit for some jobs. Examples are for Ubuntu 24.04.

Short answer: A cron job is a command the Linux cron service runs automatically on a fixed recurring schedule. Run crontab -e and add a line with five time fields (minute, hour, day of month, month, day of week) followed by the command; for example, 30 2 * * * /usr/local/bin/backup.sh runs every day at 2:30. Because cron doesn't load your shell environment, use full paths to commands and redirect output to a log file.

Crontab syntax: the five fields

text
┌───────────── minute (0-59)
│ ┌─────────── hour (0-23)
│ │ ┌───────── day of month (1-31)
│ │ │ ┌─────── month (1-12)
│ │ │ │ ┌───── day of week (0-7; both 0 and 7 are Sunday)
│ │ │ │ │
* * * * *  command

Operators you can use in any field:

  • * means "every value".
  • , is a list: 1,15 means the 1st and the 15th.
  • - is a range: 1-5 means Monday to Friday.
  • / is a step: */10 in the minute field means every 10 minutes.

Crontab examples

Expression Meaning
* * * * * Every minute
*/5 * * * * Every 5 minutes
0 * * * * At the top of every hour
30 2 * * * Every day at 2:30
0 9-17 * * 1-5 On the hour from 9 to 17, Monday to Friday
0 0 * * 0 Midnight every Sunday
0 3 1 * * 3 a.m. on the first day of every month
15 4 * * 6 4:15 every Saturday
0 */6 * * * Every 6 hours (0, 6, 12, 18)
@reboot Once after every boot
@daily Same as 0 0 * * *

Two things that catch people out:

  • The week starts on Sunday. 0 is Sunday and 6 is Saturday.
  • Day of month and day of week combine with OR, not AND. 0 0 13 * 5 means the 13th of every month and also every Friday, not only Fridays that fall on the 13th.

Cron uses the system clock. If the server runs on UTC (a good choice for servers), 30 2 means 2:30 UTC. Check the server's time zone with timedatectl.

crontab -e and crontab -l

Each user has their own crontab, and its jobs run with that user's permissions:

bash
crontab -e      # edit the current user's crontab
crontab -l      # list the current user's crontab
sudo crontab -u deploy -l   # list another user's crontab

The first time you run crontab -e, it asks which editor to use. Once you save and exit, cron picks up the changes on its own; there's no service to restart.

Be careful with crontab -r: it deletes the entire crontab without confirmation, and r sits right next to e on the keyboard. A good habit is to save the output of crontab -l to a file now and then, or better, put important jobs in /etc/cron.d, where they are plain files you can keep in git.

/etc/cron.d: system jobs with a user field

Files in /etc/cron.d use the same format as a crontab, with one important difference: after the five time fields there is a user name field.

/etc/cron.d/myapp
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# Laravel scheduler, every minute
* * * * * deploy cd /var/www/myapp && php artisan schedule:run >> /dev/null 2>&1

# Nightly backup
30 2 * * * root /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

A few rules that, if broken, cause the file to be silently ignored:

  • The file must be owned by root and writable only by root (chmod 644).
  • The file name may contain only letters, digits, - and _. On Ubuntu, a file with a dot in its name (such as myapp.cron or backup.bak) is not run.
  • The last line must end with a newline.

The advantage of /etc/cron.d is that each application gets its own file, deployment tools can install it, and you can see every job by looking in one folder. There are also /etc/cron.daily, /etc/cron.weekly and similar folders, whose executable scripts run on a fixed schedule; they suit jobs where the exact time doesn't matter.

Why does it work in the terminal but not in cron?

The answer is almost always one of these three.

1. PATH is minimal

Cron doesn't read files like ~/.bashrc or ~/.profile, and its default PATH is just /usr/bin:/bin. A command in /usr/local/bin or somewhere like ~/.local/bin won't be found. You have two options: use full paths everywhere (/usr/local/bin/composer), or define PATH at the top of the crontab as in the example above. Any other variables your program needs must also be defined there or inside the script itself.

2. The shell is sh, not bash

Cron's default shell is /bin/sh, which on Ubuntu is dash. Things like [[ ... ]], source or arrays don't work in it. Either put SHELL=/bin/bash at the top of the file or, better, write the logic in a script with #!/bin/bash and have cron call only that script.

3. % has a special meaning

In a crontab line, % means "newline", and everything after the first % is passed to the command as standard input. This line doesn't work:

text
0 3 * * * tar czf /backup/site-$(date +%F).tar.gz /var/www

You need to escape % with \:

text
0 3 * * * tar czf /backup/site-$(date +\%F).tar.gz /var/www

Again, the cleaner approach is to put such commands in a script; inside a script, % has no special meaning.

Cron job output and logs

Cron emails each job's output to the user who owns it, provided a mail transfer agent is installed on the server. On most servers it isn't, so output and errors are silently discarded. Always redirect output explicitly:

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

>> appends to the file and 2>&1 sends errors to the same place. If these logs grow large, add a file for them in /etc/logrotate.d.

To check whether cron ran the job at all, look at the service's own log:

bash
journalctl -u cron --since today

If you see a CMD (...) line for your job, cron did its part and the problem is in the command itself. If there's no line at all, the problem is the schedule, the file name or the user field.

A simple debugging trick is to run the script in a cron-like environment:

bash
env -i SHELL=/bin/sh PATH=/usr/bin:/bin HOME="$HOME" /bin/sh -c '/usr/local/bin/backup.sh'

Preventing overlapping runs with flock

Cron doesn't know whether the previous run has finished. If a job runs every 5 minutes and one run takes 7 minutes, two copies run at once; for backups or syncs, that means corrupted files or duplicated work. The flock utility (installed on Ubuntu by default) solves this:

text
*/5 * * * * deploy /usr/bin/flock -n /run/lock/myapp-sync.lock /usr/local/bin/sync.sh >> /var/log/myapp-sync.log 2>&1

flock takes a lock on the file before running the command. -n means if someone else holds the lock, don't wait; just exit quietly. The lock is released automatically when the command finishes, even if it fails, so a leftover lock file causes no trouble.

The alternative: systemd timers

Ubuntu uses systemd, and systemd has its own scheduler. For each job you create two files: a .service that says what to run and a .timer that says when.

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

[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/backup.sh
/etc/systemd/system/backup.timer
[Unit]
Description=Run backup every night at 02:30

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

[Install]
WantedBy=timers.target

Enable and check it:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers

systemctl list-timers shows when each timer last ran and when it runs next. To test, start the service manually without waiting for its schedule and check the log:

bash
sudo systemctl start backup.service
journalctl -u backup.service -n 50

The OnCalendar format differs from cron's but is more readable: Mon..Fri *-*-* 09:00:00 means weekdays at 9, and hourly, daily and weekly are accepted too. If you're unsure what an expression means, systemd-analyze calculates its next run times:

bash
systemd-analyze calendar "Sat *-*-* 04:15:00" --iterations=3

Cron or systemd timer?

cron systemd timer
Setup One line Two files
Logging Must be redirected by hand Automatic, in the journal
Overlapping runs Prevented with flock Won't start again until the previous run finishes
Missed runs (server was off) Not caught up Run after boot with Persistent=true
Resource limits, dependencies on other services None Everything systemd offers

For quick, simple jobs, cron is still the shortest path. For important jobs such as backups, where logging, overlap prevention and catching up on missed runs matter, a systemd timer is the better choice.

Frequently asked questions

How do I run a cron job every 5 minutes?

Run crontab -e and add a line starting with */5 * * * * followed by the full path to the command or script. Cron picks up the change on save, so there is nothing to restart. If a run might take longer than 5 minutes, wrap the command in flock -n so two copies never run at once.

How do I check if a cron job ran?

Check the service log with journalctl -u cron --since today. If there is a CMD line for your job, cron ran it and the problem is inside the command; if there is no line at all, the issue is the schedule, the file name or the user field.

What is the difference between crontab -e and /etc/cron.d?

crontab -e edits the current user's personal jobs, which run with that user's permissions. Files in /etc/cron.d are system jobs with an extra user field after the five time fields; they must be owned by root, and their names must not contain a dot.

What time zone do cron jobs use?

Cron uses the system clock and time zone. If the server is on UTC, 30 2 means 2:30 UTC, not your local time. Check the server's time zone with timedatectl.

Does a cron job run later if the server was off at the scheduled time?

No. Cron doesn't catch up on missed runs; the job simply waits for its next scheduled time. If that matters, use a systemd timer with Persistent=true, which runs the missed job after the server boots.

Wrap-up

  • Five fields: minute, hour, day of month, month, day of week. Sunday is 0, Saturday is 6.
  • crontab -e for personal jobs; /etc/cron.d for system jobs, with a user field and a file name without dots.
  • Cron doesn't read your shell profile: set PATH, use full paths and escape %. Best of all, put the work in a script.
  • Always send output to a log file, and use flock for frequently repeating jobs.
  • For critical jobs, a systemd timer gives you logging, overlap prevention and catch-up runs at no extra effort.

If you want to set up a real job right now, a nightly backup is the best place to start; Backups with the 3-2-1 rule covers what to keep and where. For background work inside the application itself, see Queues in Laravel.