To create a cron job, you tell Linux in a single line which command should run and when. A backup every morning at 4, a health check every five minutes, a report every Monday. You open your personal schedule table with crontab -e, write five time fields plus the command, and save. Done. That really is all you need for your first cron job.
The hard part comes afterwards. Cron is a very old, very quiet tool: when a job doesn’t run, it rarely tells you. Our own server runs 41 cron jobs in the root crontab plus eight files in /etc/cron.d/. For this article, on 6 October 2026, we built test jobs that deliberately walk into the well-known traps, and we took a hard look at our own crontab. Three findings up front:
1. A job that runs longer than its interval is still started again every minute by cron. In our test, three copies were running at the same time after three minutes. Cron has no overlap protection.
2. A file in
/etc/cron.d/without a trailing newline is ignored entirely, and so is a file with a dot in its name. In the first case there’s at least a log line. In the second, nothing at all.3. Cron mails any output to the job’s owner. On our server those mails have been going to an address that doesn’t exist for weeks: 273 bounces in one month that nobody ever read.

What is a cron job?
Cron is a background service (a “daemon”) that runs on practically every Linux and Unix system. It wakes up every minute, checks its tables and starts everything whose time has come. A cron job is a single entry in one of those tables, and the table itself is called a crontab (short for cron table). The name comes from the Greek chronos, time.
On Ubuntu and Debian the package is simply called cron; on Fedora, Rocky Linux and Arch it’s usually cronie. Usage is the same everywhere. Our test system: Ubuntu 24.04 with cron 3.0pl1-184ubuntu2. To check whether the service is running:
systemctl status cron # Ubuntu, Debian
systemctl status crond # Fedora, Rocky, Arch (cronie)
Ours had been running for three weeks without a break and had started 2,928 commands in the past 24 hours. That sounds like a lot, but it’s normal for a server with a few check scripts running every three or five minutes.
If you’re only just setting up your server, our guide to Linux server setup covers what comes first. For the basics of working on a remote machine, see What is SSH.
Jay LaCroix of Learn Linux TV walks through cron step by step, including the system-wide files. Thorough and calm.
Create a cron job: the quick guide
If you just want to create a cron job quickly, these four steps are enough:
1. Open your crontab:
crontab -e
The first time, Ubuntu asks for an editor. For beginners, nano is the friendliest choice (save with Ctrl+O, exit with Ctrl+X). You can switch later with select-editor.
2. Add a line at the end, for example a daily script at 4:30 am:
30 4 * * * /home/fabian/bin/backup.sh >> /home/fabian/logs/backup.log 2>&1
3. Save and close. Cron reports crontab: installing new crontab. You do not need to restart the service; cron picks up changed tables on its own.
4. Check that the entry arrived:
crontab -l
That’s it. The rest of this article explains what the five asterisks mean, why the >> ... 2>&1 at the end isn’t decoration, and why your script works in the terminal but not in cron.
The most important crontab commands
| Command | What it does |
|---|---|
crontab -e | edit your own crontab (creates it the first time) |
crontab -l | list your own crontab |
crontab -r | delete your whole crontab, without asking |
crontab -i -r | same, but asks first |
sudo crontab -u alice -e | edit another user’s crontab |
crontab file.txt | replace your crontab with the contents of a file |
A word on crontab -r: on the keyboard, r sits right next to e. One typo and all your jobs are gone. So before bigger changes we take a copy:
crontab -l > ~/crontab-backup-$(date +%F).txt
Restore with crontab ~/crontab-backup-2026-10-06.txt.
Crontab syntax: five fields, one command
Every line in a crontab consists of five time fields followed by the command:
┌───────────── minute (0–59)
│ ┌─────────── hour (0–23)
│ │ ┌───────── day of month (1–31)
│ │ │ ┌─────── month (1–12 or jan–dec)
│ │ │ │ ┌───── day of week (0–7, 0 and 7 = Sunday, or sun–sat)
│ │ │ │ │
* * * * * command
An * means “every value”. So * * * * * command runs every minute of every day of every month.

Special characters
| Character | Meaning | Example | Runs |
|---|---|---|---|
* | every value | * * * * * | every minute |
, | list | 0 8,12,18 * * * | at 8, 12 and 18 |
- | range | 0 9-17 * * 1-5 | hourly 9–17, Mon–Fri |
/ | step | */15 * * * * | every 15 minutes |
| combined | range with step | 0 8-20/4 * * * | 8, 12, 16, 20 |
Cron job examples to copy
| Schedule | Crontab line |
|---|---|
| every minute | * * * * * |
| every 5 minutes | */5 * * * * |
| every hour on the hour | 0 * * * * |
| every day at 3:00 | 0 3 * * * |
| every day at 3:00 and 15:00 | 0 3,15 * * * |
| weekdays at 7:30 | 30 7 * * 1-5 |
| every Sunday at 4:47 | 47 4 * * 0 |
| 1st of every month at midnight | 0 0 1 * * |
| every 2 hours at :45 | 45 */2 * * * |
| once a year on 1 January | 0 0 1 1 * |
A practical tip: avoid round times unless you need them. At midnight and on every full hour a server already has plenty going on. On our test morning, ten jobs started at the same second at 4:00:01. Our own maintenance jobs deliberately sit on odd minutes like :23, :41 or :47. If you want to double-check an expression before saving, crontab.guru translates it into plain English.
Shorthand strings
Instead of five fields you can use one of these keywords:
| Keyword | equivalent |
|---|---|
@reboot | once when the cron service starts |
@hourly | 0 * * * * |
@daily / @midnight | 0 0 * * * |
@weekly | 0 0 * * 0 |
@monthly | 0 0 1 * * |
@yearly / @annually | 0 0 1 1 * |
We use @reboot twice, for example for a protective check shortly after boot. Note that @reboot means “when cron starts”, not “when the network is ready”, which is why we prefix it with sleep 30 &&. For anything that must reliably start after the network or a database, a systemd service with After= is the better tool.
The day-of-month vs. day-of-week trap
This rule surprises almost everyone: when both day of month and day of week are set (neither is *), cron combines them with OR, not AND.
We tested it live. The line
* * 1 * 2 command
looks like “on the 1st of the month, if it’s a Tuesday”. Our test day was Tuesday 6 October, definitely not the first of a month. The log said:
07:07:01 dom1-or-tuesday
The job ran. It runs on every 1st of the month and additionally on every Tuesday. The comparison line * * 1 * * (just the 1st) did not run that day, as expected.
If you really want “first Monday of the month”, put the weekday into the command as a condition:
0 9 1-7 * * [ "$(date +\%u)" = 1 ] && /path/to/script.sh
Start on days 1 to 7, and only continue if today is Monday (%u = 1). We’ll get to that \% in a moment.
Where cron jobs live: user crontabs and system files
There isn’t just one crontab. On a typical Ubuntu server you’ll find cron jobs in these places:
| Location | Edit with | User field? | Typical purpose |
|---|---|---|---|
/var/spool/cron/crontabs/NAME | crontab -e | no | a user’s personal jobs |
/etc/crontab | editor (as root) | yes | system-wide main table |
/etc/cron.d/* | editor (as root) | yes | jobs shipped by packages |
/etc/cron.hourly/, .daily/, .weekly/, .monthly/ | drop a script in | – | whole scripts without their own schedule |
Never edit the files under /var/spool/cron/crontabs/ directly; always use crontab -e. Only then does crontab check the syntax and cron reliably notice the change.
The key difference: in /etc/crontab and /etc/cron.d/ there’s a username between the schedule and the command:
# /etc/cron.d/example
30 4 * * * root /usr/local/bin/backup.sh
A user crontab (crontab -e) has no such field. Copy a line from there into /etc/cron.d/, forget the user, and you get a job that tries to run as a user called /usr/local/bin/backup.sh and then does nothing.
Our /etc/cron.d/ contains eight files, most from packages: certificate renewal (certbot), PHP session cleanup, system statistics (sysstat), webmail cleanup. The daily folder /etc/cron.daily/ holds logrotate, apt-compat and man-db, among others. When they run is defined in /etc/crontab; on Ubuntu, daily at 6:25 unless anacron is installed.
Two silent traps in /etc/cron.d/
We triggered both on purpose.
First: the last line needs a newline. We wrote a file whose only line ended without a final Enter. Cron did not run it. The journal said:
cron[514899]: (*system*gm-cron-nonl) ERROR (Missing newline before EOF,
this crontab file will be ignored)
The whole file is ignored, not just the last line. This typically happens when files are generated by a script with printf or echo -n. crontab -e protects you; most normal editors do too.
Second: no dots in the file name. On Debian and Ubuntu, cron ignores every file in /etc/cron.d/ whose name contains a dot (meant to skip package backups like .dpkg-old). Our file gm-cron.dot, containing a perfectly valid job, never ran once, and this time there was no log line at all. A file called backup.cron or myjob.sh in /etc/cron.d/ is dead without you ever noticing. Allowed characters are letters, digits, hyphens and underscores.
Why the cron job works in the terminal but not in cron
This is by far the most common cron question, and the answer is nearly always the same: cron runs your command in an almost empty environment. No .bashrc, no .profile, no aliases, no Node version manager, a short PATH.
We looked at what a cron job actually sees on our server. The test job env > /tmp/gmcron/env.txt produced exactly ten variables:
HOME=/root
LOGNAME=root
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:...
SHELL=/bin/sh
LANG=en_US.UTF-8
PWD=/root
DISPLAY=:99
ANDROID_HOME=...
By comparison, the interactive shell on the same server had six extra PATH entries: ~/.local/bin, ~/.npm-global/bin, ~/.bun/bin, the asdf and volta shims, ~/.nvm/current/bin. Anything you installed with npm install -g, pip install --user or a version manager is invisible to cron.
The only reason our cron PATH has more than the Debian defaults is /etc/environment: on Debian and Ubuntu, cron reads that file via PAM. Handy, but a quirk of those distributions. On other systems you may get nothing beyond /usr/bin:/bin.

Other differences that regularly break jobs:
- The shell is
/bin/sh, not Bash. On Ubuntu that’sdash. Bash features like[[ ... ]],source,{1..5}or$RANDOMdon’t work or behave differently. Either putSHELL=/bin/bashat the top of the crontab or (better) move the command into a script with#!/bin/bash. - The working directory is your home directory. Relative paths like
./data/export.csvend up somewhere you didn’t expect. In the script, start withcd /path/to/project || exit 1. - No environment variables from
.bashrc. API keys,NODE_ENV, proxy settings: whatever you export there, cron doesn’t see. - No terminal. Programs that prompt interactively or want to print colours behave differently or abort.
How to make a cron job robust:
# At the top of the crontab (applies to every line below)
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
# Always absolute paths, and a dedicated script instead of long one-liners
30 4 * * * /home/fabian/bin/backup.sh >> /home/fabian/logs/backup.log 2>&1
And in the script itself:
#!/bin/bash
set -euo pipefail
cd /home/fabian/project
export NODE_ENV=production
/home/fabian/.nvm/versions/node/v22.20.0/bin/node export.js
There’s a simple trick to test whether a script survives the bare environment: run it the way cron would.
env -i HOME="$HOME" LOGNAME="$USER" PATH=/usr/bin:/bin SHELL=/bin/sh \
/bin/sh -c '/home/fabian/bin/backup.sh'
If it works here, it works in cron.
The percent sign: the bug you can’t see
One character has a special meaning in a crontab that almost nobody knows about: % is turned into a newline. Everything after the first % is fed to the command as standard input, not as part of the command line.
We ran two almost identical test lines:
* * * * * date +%H:%M:%S.%N >> /tmp/gmcron/start.txt
* * * * * date +\%H:\%M:\%S.\%N >> /tmp/gmcron/start-escaped.txt 2>&1
The second file filled up with timestamps every minute. The first file was never created. The cron log showed the command as just:
(root) CMD (date +)
Cron had cut the line at the first %. The redirect >> /tmp/gmcron/start.txt came after the %, so it became input for date, which ignores input. No file, no error in any file, and as far as cron was concerned the job ran.
The fix: in crontab lines, write every % as \%. Or, again better, move the date call into a script. The special rule doesn’t apply inside scripts.
Output and logs: where the output goes
Cron captures everything your command writes to standard output or standard error and sends it by email to the crontab owner (or the address in MAILTO). That’s a legacy from the days when every Unix machine had local mail.
On a modern server, there are three possible outcomes:
- No mail program installed: cron discards the output and at most logs
(CRON) info (No MTA installed, discarding output). - Mail program installed and configured correctly: the mail arrives. That’s the intended case, but it’s rare.
- Mail program installed, but configured for something else: the mail goes somewhere. That was our case.

Our finding: 273 mails into the void
Our server runs a mail server (Postfix) for another project. A system job in /etc/cron.d/ that starts three short commands three times a day produces output. Cron dutifully wraps it in a mail to root. Postfix appends its default domain to root, and that domain belongs to an old project whose mailbox has no user called root. The mail log says:
status=bounced (... said: 550 5.1.1 <root@...> User doesn't exist)
Counted across the mail logs from 6 September to 6 October: 273 bounces for orig_to=<root>, a steady nine per day (at 4:00, 12:00 and 20:00, three each). Each bounce also generates an error notice that can’t be delivered either. Nobody ever read any of this output. Had the job reported a real error, it would have vanished in exactly the same place.
We deliberately changed nothing while writing this article; cleaning that up is a separate decision. But the lesson applies to everyone: if you aren’t completely sure where your server’s cron mail goes, don’t rely on it.
The better way: redirect output yourself
Of the 41 jobs in our root crontab, 34 redirect their own output: 21 into their own log file, 13 to /dev/null. Seven write nowhere and so depend on the mail path. The patterns:
# Append everything to a log file (default for important jobs)
30 4 * * * /path/backup.sh >> /var/log/backup.log 2>&1
# Keep only errors, discard normal output
*/5 * * * * /path/check.sh > /dev/null 2>> /var/log/check-errors.log
# Discard everything (only if the script logs on its own!)
*/3 * * * * /path/ping.sh > /dev/null 2>&1
# Send to the system journal under its own tag
0 3 * * * /path/cleanup.sh 2>&1 | logger -t cleanup
Order matters in >> file 2>&1: first send standard output to the file, then send standard error to wherever standard output already goes. The other way round (2>&1 >> file), errors still end up in the mail.
We like the logger variant best, because the output then sits in the journal next to everything else, readable with journalctl -t cleanup and rotated automatically.
Log files that cron appends to for years grow without limit. Either hook them into logrotate or trim them in the script.
Did my cron job run at all?
Cron logs every start. On Ubuntu, look in the journal:
journalctl -u cron --since "1 hour ago"
grep CRON /var/log/syslog | tail -20 # if rsyslog is running
A line looks like this:
Oct 06 07:04:01 Nyx CRON[1563376]: (root) CMD (env > /tmp/gmcron/env.txt 2>&1)
Important: this line only proves that cron started the command. Whether it succeeded isn’t recorded. Our % test job showed up as CMD (date +) every minute and never did anything. You can only verify success through the job’s own output, i.e. your log file or an exit code you store somewhere.
Prevent overlapping runs: flock
Cron starts a job at its scheduled time whether or not the previous run is still going. For jobs that are usually quick but occasionally hang (slow network, locked database, backup bigger than expected) that’s dangerous. Two backups writing into the same file at once are worse than none.
Our test: a job that starts every minute and sleeps for 150 seconds. The log:
07:03:01 start 1562528
07:04:01 start 1563372
07:05:01 start 1564161
07:05:31 end 1562528
07:06:01 start 1565253
At 7:05:01, three copies were running at once. Cron didn’t even issue a warning.

The fix is flock (part of util-linux, available on every Linux). It takes a lock file and only runs the command if nobody else holds the lock:
* * * * * flock -n /tmp/myjob.lock /path/to/script.sh
-n means: if the lock is taken, give up immediately instead of waiting. Our second test job with the same run time, this time with flock -n:
07:03:01 locked-run
07:04:01 skipped
07:05:01 skipped
Exactly one run; the others were skipped cleanly. Because the kernel releases the lock when the process ends, it doesn’t get stuck after a crash either; a leftover .lock file is harmless.
By the way, none of the 41 jobs in our own crontab uses flock. For most that’s fine, as they take seconds and run every few minutes. But that’s exactly the assumption nobody checks: a script that runs every three minutes and needs four minutes on a bad day will silently pile up.
Other useful companions:
# Abort after 10 minutes at most
*/15 * * * * timeout 600 /path/script.sh
# Run with low priority (CPU and disk)
0 3 * * * nice -n 10 ionice -c3 /path/backup.sh
# Combined
0 3 * * * flock -n /tmp/backup.lock timeout 2h nice -n 10 /path/backup.sh >> /var/log/backup.log 2>&1
Time zones and daylight saving time
Cron uses the system time zone. timedatectl tells you which one that is. Our servers, like most cloud servers, run on UTC. 0 4 * * * then means 4:00 UTC, which is 6:00 in summer and 5:00 in winter in Central Europe. If you schedule jobs for people (“report at 8 am”), you have to convert or set the time zone.
If a server runs on a local zone such as Europe/Berlin or America/New_York, daylight saving time comes into play: in spring one hour is skipped, in autumn one hour happens twice. Debian’s cron tries to handle both (skipped jobs are caught up, repeated ones aren’t run twice); other cron implementations behave differently. The simplest rule: don’t schedule important jobs between 1 and 3 am if the server isn’t on UTC.
Some cron versions (cronie on Fedora and Rocky Linux) understand CRON_TZ=Europe/Berlin in the crontab. Debian’s cron on Ubuntu ignores that variable. So don’t rely on it without testing.
A one-off job is a yearly job
Cron has no concept of a year. If you create a one-off appointment as a cron job, say a reminder on 23 March at 9 pm:
0 21 23 3 * command
you get that reminder every year on 23 March until someone deletes the line. Going through our own root crontab, we found six lines like that: reminders and one-off actions from spring and summer whose occasion is long gone. Two of them would send a message next March to a phone number that no longer exists. Another one copies a backup file back into place every 25 July. All six sit quietly until their date comes round again, and then they do something nobody wants any more.
For genuine one-off tasks there are two better tools:
# at: run once, then forget
echo "/path/script.sh" | at 21:00 2026-10-24
# systemd-run: one-off timer, shows up in systemctl list-timers
sudo systemd-run --on-calendar="2026-10-24 21:00" /path/script.sh
If you still use cron, have the job remove itself, or at least note the year in a comment so the next clean-up knows what can go.
Security: who may create cron jobs?
A few points worth knowing:
- Jobs in root’s crontab run as root. A script that root runs every five minutes but that a regular user can edit is an open door. Check the permissions of the scripts you call:
ls -l /path/script.shshould showrootas owner and not be writable by others. /etc/cron.allowand/etc/cron.denycontrol which users may usecrontabat all. Ifcron.allowexists, only users listed there may. Useful on multi-user systems, usually unnecessary on a one-person server.- No passwords in crontab lines. Any process on the system can see the full command line of running jobs with
ps, and the line ends up in the cron log. Secrets belong in a file with600permissions that the script reads. - Cron is a favourite hiding place for attackers. Whoever takes over a server likes to add a cron job that restores the backdoor regularly. In a security incident,
crontab -lfor every user,/etc/crontaband/etc/cron.d/are among the first places to look. More on that in our report on a server that was hijacked for five days.
All user crontabs at a glance:
sudo ls -l /var/spool/cron/crontabs/
for u in $(sudo ls /var/spool/cron/crontabs/); do echo "== $u"; sudo crontab -u "$u" -l; done
On our server there are two: root and a service user for one project that renews its access token every four hours.
A compact beginner introduction from Akamai’s developer channel (formerly Linode). The cloud provider is the publisher, but the content is vendor-neutral.
Cron job vs. systemd timer vs. anacron
Cron isn’t the only way to run things on a schedule. Modern Linux systems offer two alternatives:
| Cron | systemd timer | anacron | |
|---|---|---|---|
| Effort per job | 1 line | 2 files | 1 line |
| Logs | redirect yourself | automatic, in the journal | redirect yourself |
| Catch up missed runs | no | yes (Persistent=true) | yes, that’s its purpose |
| Overlap | possible, needs flock | impossible | – |
| Precision | minute | second and finer | day |
| Resource limits | via nice/timeout | MemoryMax=, CPUQuota= | no |
| Dependencies (network, DB) | no | After=, Requires= | no |
| Overview | crontab -l | systemctl list-timers | /etc/anacrontab |
anacron is meant for machines that aren’t always on (laptops, desktops): it remembers when a daily job last ran and catches up on the next boot. On a server that runs continuously you rarely need it.
We tested systemd timers in detail in our article on how to create a systemd service. They solve almost every problem in this article out of the box: output lands in the journal, runs never overlap, the environment is clearly defined, and systemctl list-timers shows when the next run is and when the last one was.
Our honest practice: cron for small stuff, timers for anything important. A health check every five minutes, a cache cleaner, a short sync script: cron, one line, done. Backups and anything where a silent failure really hurts: timer.
Cron jobs without your own server
Not everyone has root access. Three typical cases:
- Shared hosting (hosts with an admin panel): almost every host offers a “Cron jobs” or “Scheduled tasks” section. You enter the schedule and the command or a URL. The fields are the same five as in this article.
- WordPress and other web apps often ship their own pseudo-cron that only runs when someone visits the site (
wp-cron.php). On low-traffic sites, scheduled tasks then run unreliably. The usual fix: disable the built-in mechanism and set up a real cron job that calls the site every five minutes. - Docker containers normally have no cron service. Either start the job on the host with
docker exec, or run a small dedicated container that only runs cron. More on containers in What is Docker.
Troubleshooting checklist
When a cron job doesn’t do what it should, we go down this list:
- Is the cron service running?
systemctl status cron - Is the job installed?
crontab -l(as the right user!) or the file in/etc/cron.d/ - Was it started?
journalctl -u cron --since today | grep PARTOFCOMMAND. Nothing there: check the schedule, the file name (dot?) or a missing trailing newline. A truncated command: escape%. - What did it print? Redirect output to a log file (
>> /tmp/debug.log 2>&1) and wait a minute. - Does it work in cron’s environment? Test with the
env -icall from above. Typical message:command not found→ absolute path or setPATH. - Are the permissions right? Script executable (
chmod +x)? Can the user access all files? - Is the time right?
timedatectl: is the server perhaps on UTC? - Is it running twice?
ps aux | grep scriptname. If so:flock.
A trick for quick testing: temporarily set the schedule to * * * * *, wait a minute, read the log, then set it back. That’s exactly how the tests in this article were done.
Frequently asked questions
How do I create a cron job in Linux?
Open your crontab with crontab -e, add a line with five time fields and the command at the end (for example 30 4 * * * /path/script.sh >> /path/log.txt 2>&1), and save. Cron picks up the change automatically; no restart needed.
What do the five asterisks in a crontab mean?
Minute, hour, day of month, month, day of week, in that order. An * means “every value”. So * * * * * runs every minute and 0 3 * * * every day at 3:00.
How do I run a cron job every 5 minutes?
With */5 * * * * command. It then runs at minutes 0, 5, 10, 15 and so on every hour. Every 15 minutes is */15, every 2 hours 0 */2 * * *.
Where are cron job logs?
Cron only logs that a job started; you’ll find that with journalctl -u cron or in /var/log/syslog. You have to redirect the job’s own output, for example with >> /var/log/myjob.log 2>&1 or | logger -t myjob, otherwise it gets mailed to the user or disappears.
Why is my cron job not running?
Usually because of the bare environment: short PATH, /bin/sh instead of Bash, the home directory as working directory, no variables from .bashrc. Other common causes: an unescaped % in the line, a dot in a file name under /etc/cron.d/, a missing newline at the end of a file, or the wrong time zone.
Do I need to restart cron after a change?
No. crontab -e notifies cron of the change, and /etc/cron.d/ and /etc/crontab are checked for changes every minute. In our tests, new files ran from the next full minute.
How do I stop a cron job from running twice?
With flock -n /tmp/name.lock command. If the previous run is still going, the new one is skipped immediately. Without flock, cron starts a new copy every minute even if the old one is still working; in our test there were three at once after three minutes.
How do I delete a cron job?
Remove the line with crontab -e or comment it out with #, then save. crontab -r, on the other hand, deletes the entire crontab without asking, so back it up first with crontab -l > backup.txt.
How do I run a cron job as a different user?
As root, edit that user’s crontab with sudo crontab -u username -e. Or create a file in /etc/cron.d/ and put the username between the schedule and the command: 30 4 * * * username /path/script.sh.
Can a cron job run every second?
Not directly; the smallest unit is one minute. Workarounds with several lines of sleep 10; command exist but are ugly. For intervals under a minute, a systemd timer (OnUnitActiveSec=10s) or a permanently running service is the better choice.
Which is better, cron or a systemd timer?
Cron is simpler (one line) and the same everywhere. systemd timers give you logs in the journal, catch-up of missed runs, overlap protection and resource limits. For small things cron is fine; for backups and important tasks we use timers.
Conclusion: one line for the job, three habits for peace of mind
Creating a cron job takes a minute: crontab -e, five time fields, one command, save. And it runs. What our tests showed, though, is that cron stays quiet when things go wrong.
- Cron starts a job even if the previous run hasn’t finished. We had three copies running in parallel until
flockstopped it. - A
%in the line truncates the command without an error showing up anywhere. - Files in
/etc/cron.d/with a dot in the name or without a final newline are ignored, once with a log line, once without. - Day of month and day of week together mean OR, not AND.
- Output is mailed to an address that perhaps nobody reads: 273 bounces in one month on our server.
- A one-off date comes back every year: six forgotten lines in our own crontab.
The three habits that catch almost all of this: absolute paths in a dedicated script, always redirect output yourself, and flock for anything that might take more than a few seconds. For truly important tasks, moving to a systemd timer is worth it. And if SSH logins are pouring into your server’s logs, Fail2ban is a good candidate for your next automation, right after a clean UFW firewall.