Time

Cron Expression Guide for Busy Developers and Server Admins

Cron syntax is compact but easy to misread. This guide breaks it into clear, practical scheduling patterns.

Cron syntax is compact but easy to misread. This guide breaks it into clear, practical scheduling patterns.

This article focuses on the practical side of the workflow, including where developers usually get stuck, what to verify before trusting the result, and how the topic shows up in day-to-day implementation work.

Why Cron Still Runs the World

If you have ever logged into a Linux box and typed crontab -e, you have touched a piece of software design that has barely changed since the 1970s. Cron is the time-based job scheduler baked into virtually every Unix-like system, and despite a flood of modern alternatives like systemd timers, Kubernetes CronJobs, and managed cloud schedulers, the humble five-field cron expression remains the lingua franca. Learn it once and you can read a backup script on a CentOS server, a GitHub Actions workflow trigger, and an AWS EventBridge rule without missing a beat.

The appeal is brutal simplicity. A single line tells the daemon what to run and exactly when. There are no dependencies to install, no service to babysit, and the syntax fits in your head after an afternoon of practice. The trade-off is that the syntax is terse to the point of being cryptic, and a single misplaced character can mean a job fires every minute instead of once a day.

This guide walks through the five fields, the special characters that make expressions expressive, the patterns you will reach for constantly, and the handful of gotchas that have burned every admin at least once. The goal is that you leave able to write and verify a schedule with confidence rather than copying a line from a forum and hoping.

The Five Fields, Left to Right

A standard cron expression is five whitespace-separated fields. In order, they are: minute (0-59), hour (0-23), day of month (1-31), month (1-12), and day of week (0-7, where both 0 and 7 mean Sunday). After those five fields comes the command to execute. So a complete crontab line looks like 0 2 * * * followed by the path to a backup script.

Reading left to right, that example says minute 0, hour 2, any day of month, any month, any day of week. In plain English: run at 2:00 AM every single day. The fields are evaluated together as an AND condition for the time fields, with one important exception involving the two day fields that we will cover shortly.

A useful habit is to read each field aloud as a constraint. An asterisk means no constraint, so the more asterisks you have, the more often a job fires. The expression * * * * * has zero constraints and therefore runs once every minute, which is the densest schedule cron natively supports. Many beginners write that by accident when they actually wanted a daily job and just forgot to pin the hour and minute.

Special Characters That Do the Heavy Lifting

Five characters give cron expressions their power. The asterisk matches every value in a field. The comma creates a list, so 0,15,30,45 in the minute field means run at the top of the hour and every quarter past. The hyphen defines an inclusive range, so 1-5 in the day-of-week field means Monday through Friday.

The slash is a step value and is the one people misuse most. Written as */15 in the minute field, it means every 15 minutes starting from 0, producing 0, 15, 30, and 45. You can combine it with a range too: 0-30/10 means every 10 minutes within the first half of the hour. A common mistake is assuming */15 means fifteen minutes after the job last ran. It does not. Cron has no memory of previous runs; it simply checks whether the current wall-clock minute matches the pattern.

These characters compose freely. The expression 0 9-17 * * 1-5 reads as minute 0, every hour from 9 AM through 5 PM, on weekdays, which is a tidy way to fire a job hourly during business hours. Once the four operators click, most schedules you encounter become readable at a glance.

Patterns You Will Write Again and Again

A small vocabulary of expressions covers the majority of real-world needs. Memorize these and you will rarely need a reference. For frequent polling, */5 * * * * runs every 5 minutes and */15 * * * * runs every 15 minutes. For a heartbeat job, 0 * * * * runs once an hour at the top of the hour.

Daily and weekly schedules are the bread and butter of maintenance work. The expression 0 2 * * * runs at 2 AM daily, a popular slot for backups because traffic is usually low. For weekdays only, 0 9 * * 1-5 fires at 9 AM Monday through Friday. A weekly job such as 0 0 * * 0 runs at midnight every Sunday, and 0 0 1 * * runs at midnight on the first day of every month, handy for monthly reports or log rotation.

Many cron implementations, including Vixie cron used on most Linux distributions, also accept named shortcuts that start with an @ symbol. These include @hourly, @daily (equivalent to 0 0 * * *), @weekly, @monthly, and @reboot, which runs once when the system boots. They read more clearly than raw numbers, but be aware that @reboot in particular is not portable and behaves differently across systems, so test it rather than assuming.

The Day-of-Month Versus Day-of-Week Trap

This is the single most misunderstood corner of cron, and it has caused real outages. The intuition most people have is that when you specify both the day-of-month field and the day-of-week field, cron treats them as an AND, running only when both match. That is wrong for the standard implementation.

When both day fields are restricted (neither is an asterisk), cron uses an OR. The job runs if either the day of month matches or the day of week matches. Consider 0 0 13 * 5 which you might intend to mean midnight on Friday the 13th. In reality it runs at midnight on the 13th of every month AND on every Friday, because either condition triggers it. To actually target Friday the 13th you cannot do it cleanly in a single standard cron line; you typically run it on every Friday and add a date check inside the script.

The practical rule is to leave one of the two day fields as an asterisk unless you genuinely want the OR behavior. If your schedule is day-of-week based, set day-of-month to an asterisk. If it is day-of-month based, set day-of-week to an asterisk. Mixing specific values in both fields almost always produces a schedule that fires more often than you expected, and the bug is invisible until the wrong day arrives.

Timezones, Daylight Saving, and the Clock You Cannot Trust

Cron runs against the system clock and the system timezone, not against any timezone you might have in your head. If your server is set to UTC but you reason about jobs in your local time, every schedule you write will be offset, and the difference can be several hours. Before writing a single expression, check what the machine thinks the time is with date and verify the configured zone with timedatectl or by reading the timezone file.

Daylight saving transitions are a genuine hazard. On the spring-forward day, the clock jumps from 1:59 AM straight to 3:00 AM, so a job scheduled for 0 2 * * * may not run at all that day because 2 AM never existed. On the fall-back day the 1 AM to 2 AM window happens twice, and depending on the implementation a job in that window may run twice or be skipped. This is exactly why 2 AM is a risky slot and why some teams deliberately schedule sensitive jobs outside the 1 AM to 3 AM band.

The robust strategy is to run servers and schedulers in UTC, which has no daylight saving, and convert to local time only at the presentation layer. If a job absolutely must align with local business hours, document the timezone assumption right next to the crontab entry. Many managed schedulers such as cloud cron services let you attach an explicit timezone to each rule, which removes the ambiguity entirely; use that feature when it exists.

The Six-Field Variant and Seconds Precision

Standard Unix cron has a resolution of one minute, which means the smallest interval you can express is every minute. That is fine for backups and reports but inadequate when you need a task to run every few seconds. To fill the gap, many libraries and frameworks extend the syntax with a sixth field placed at the front for seconds.

In a six-field expression the leading field is seconds (0-59), so 0 0 2 * * * is the six-field way of writing 2 AM daily, and */30 * * * * * means every 30 seconds. This format is common in the Quartz scheduler popular in the Java world, in the Spring framework, in the node-cron library, and in Go schedulers. Be warned that Quartz uses a slightly different convention: its day-of-week field is 1-7 with Sunday as 1, and it uses a question mark to indicate a field you are not specifying.

The lesson is that there is no single universal cron syntax. Before deploying an expression, confirm which dialect your platform expects. A five-field expression pasted into a six-field engine will be silently misread, shifting every field one position to the left and producing a schedule that bears no resemblance to what you intended. When in doubt, read the documentation for the specific scheduler rather than assuming compatibility.

Test Before You Trust

A cron expression is a tiny program, and like any program it deserves verification before it ships. The cheapest and most reliable check is to paste your expression into one of the many cron evaluators, which display the next several fire times in human-readable form. Seeing the literal dates and times a schedule will produce catches the day-field OR trap and timezone mistakes faster than any amount of staring at the raw digits.

On the server itself, validate that the daemon actually accepts and is running your entry. Use crontab -l to list the current user crontab and confirm your line is present and unmangled. Check that cron itself is running with the service status command for your distribution. Crucially, remember that cron jobs run with a minimal environment and a different PATH than your interactive shell, so always use absolute paths to binaries and scripts, and redirect output to a log file so failures are not silently swallowed.

Finally, prove the command works on its own before wiring it to a schedule. Run the exact command line by hand, ideally with a stripped environment to simulate cron sparse environment, and confirm it succeeds. The most common reason a job appears not to run is not a bad cron expression at all but a script that fails because of a missing environment variable or a relative path. Separating the scheduling question from the execution question saves hours of confused debugging.