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 -eand 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.shruns 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
┌───────────── 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,15means the 1st and the 15th.-is a range:1-5means Monday to Friday./is a step:*/10in 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.
0is Sunday and6is Saturday. - Day of month and day of week combine with OR, not AND.
0 0 13 * 5means 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:
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.
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 asmyapp.cronorbackup.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:
0 3 * * * tar czf /backup/site-$(date +%F).tar.gz /var/www
You need to escape % with \:
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:
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:
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:
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:
*/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.
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/backup.sh
[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:
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:
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:
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 is6. crontab -efor personal jobs;/etc/cron.dfor 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
flockfor 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.