What Each Scheduler Actually Does
Cron runs jobs on a fixed schedule. You specify when a job should fire using a cron expression, and the cron daemon wakes up at each matching time to execute the command. The expression is a string of five or six fields separated by spaces, each representing a unit of time.
systemd timers are a different approach. Instead of a separate daemon, they use the systemd unit system. You define a timer unit that specifies when to fire, and a service unit that runs the job. The timer and service are linked together, and systemd manages both as part of its normal boot and shutdown process.
Both approaches solve the same problem — running commands at specified times — but they do it in different ways. The choice between them depends on what you value in your workflow.
How Cron Expressions Work
A cron expression has five fields in order: minute, hour, day of month, month, and day of week. Each field accepts a value or a set of values.
The most basic expression uses a single number, which matches that exact value. For example, `0 2 * * *` runs a job at 2:00 AM every day. The asterisk means "every" in that position.
You can use a comma to list multiple values. `0,30 * * * *` runs at minute 0 and minute 30 of every hour. A hyphen specifies a range. `0 9-17 * * *` runs at 9 AM through 5 PM.
A slash specifies an interval. `*/5 * * * *` runs every five minutes. `0 */2 * * *` runs at minute 0 every two hours.
The day of week field uses numbers 0 through 6, where 0 is Sunday. The day of month field uses numbers 1 through 31. These fields can overlap — a job can fire if either the day of month or day of week matches, unless you use the `#` operator to specify which occurrence of a weekday to use.
Cron expressions have a quirk worth noting: if you specify both day of month and day of week, the job fires when either matches. This means `0 0 15 * 5` runs at midnight on the 15th of every month AND every Friday, not just when both conditions are true.
How systemd Timers Work
A systemd timer unit has a `[Timer]` section with properties that define the schedule. The most common properties are `OnCalendar` and `OnUnitActiveSec`.
`OnCalendar` uses a calendar specification that is more readable than cron expressions. The format is `YYYY-MM-DD HH:MM:SS DayOfWeek`. You can use wildcards and ranges. `Mon..Fri 09:00:00` runs on weekdays at 9 AM. `*-*-* 00:00:00` runs every day at midnight.
`OnUnitActiveSec` specifies a relative interval. If set to `5min`, the timer fires every five minutes after the previous activation. This is useful for jobs that should run at regular intervals regardless of when they started.
You can specify multiple `OnCalendar` lines in a single timer unit, and each one adds an additional trigger. This is cleaner than trying to encode multiple schedules in a single cron expression.
The timer and service share the same name prefix. If your timer is called `backup.timer`, the service is `backup.service`. They are linked automatically by systemd.
When to Choose Each
Cron is simpler for straightforward scheduling. If you need a job to run at specific times and you are comfortable with cron expressions, cron works well. It is widely available, easy to edit with a text editor, and the syntax is portable across systems.
systemd timers are better when you want tighter integration with the rest of your system. The timer and service are managed together, so you can see logs for both in `journalctl`. The calendar specification is more readable than cron expressions, especially for complex schedules. systemd also handles timezone changes more cleanly — cron jobs can drift if the system clock changes.
For jobs that need to run at regular intervals rather than at specific clock times, systemd timers are more flexible. You can combine `OnCalendar` and `OnUnitActiveSec` in a single timer unit, whereas cron requires you to choose one or the other.
Cron is also easier to debug. You can test a cron expression with tools like CronExplain, which parses the expression and shows you when the next firings occur. systemd timers require you to check the timer state with `systemctl` and review journal logs.
Common Mistakes
One common mistake with cron is assuming that day of month and day of week work as an AND condition. They do not — they work as an OR condition by default. If you want both to match, use the `#` operator to specify the occurrence.
Another mistake is forgetting that cron expressions use zero-based days of week, where 0 is Sunday. This differs from some other systems where 1 is Monday.
With systemd timers, a common mistake is confusing `OnCalendar` with `OnUnitActiveSec`. `OnCalendar` is absolute — it fires at specific clock times. `OnUnitActiveSec` is relative — it fires at intervals after the last activation. If your job takes a long time to run, `OnUnitActiveSec` can cause jobs to overlap.
Both schedulers have timezone considerations. Cron uses the system timezone by default, but you can override it with the `TZ` environment variable. systemd timers use the system timezone unless you specify otherwise in the timer unit.
Practical Advice
If you are already using systemd for other services, systemd timers are a natural fit. They are easier to manage as part of a larger systemd ecosystem, and you get automatic restart policies and logging.
If you are managing many jobs and prefer a simple text file, cron is sufficient. The crontab format is well understood and easy to version control.
For complex schedules, systemd timers are more readable. A calendar specification like `Mon..Fri 09:00:00` is clearer than a cron expression like `0 9 * * 1-5`.
Both schedulers are reliable. The choice comes down to what works best for your workflow and what you find easier to maintain.
Tool mentioned: CronExplain