The first time you see something like */15 9-17 * * 1-5, it looks like a typo. Five slots, a couple of asterisks, some numbers, a slash, a dash. The syntax is older than most production systems running it today, and it shows.
But the syntax is also small. Once you can name what each slot controls and recognize the four punctuation shortcuts, you can read almost any cron line in a few seconds. This guide walks the five fields in order, then covers the patterns that show up constantly in real crontabs and the quiet mistakes that schedule jobs to never run at all.
Photo by Alessandro Bonanni on Pexels
The five fields, left to right
A standard cron expression has five space-separated fields. They are always in the same order, and every position has a fixed range of valid values.
* * * * *
| | | | |
| | | | +-- day of week (0-6, where 0 = Sunday)
| | | +----- month (1-12)
| | +-------- day of month (1-31)
| +----------- hour (0-23)
+-------------- minute (0-59)
An asterisk means "any value." So * * * * * is "every minute of every hour of every day of every month, regardless of weekday", a job that runs 1,440 times a day. Replace asterisks with concrete numbers to narrow it down. 30 2 * * * is "minute 30, hour 2, every day": once a day at 2:30 AM.
Two oddities are worth flagging early. First, hours use a 24-hour clock, so 2 PM is 14, not 2. Second, day-of-week 0 is Sunday in classic Vixie cron and most modern descendants. Some implementations also accept 7 for Sunday as a convenience, but treating 0 = Sunday as the default keeps you safe across systems. The Wikipedia entry on cron lists the historical variants if you ever need to dig into a system that does things differently.
The four punctuation shortcuts
Once you know what each field controls, the only other vocabulary you need is the punctuation that lets you specify ranges, lists, and intervals.
Asterisk (*) matches every valid value in that field. * * * * * runs every minute. The asterisk is the cron equivalent of "any."
Comma (,) lists discrete values. 0,15,30,45 * * * * runs at minute 0, 15, 30, and 45, every quarter hour. Lists are useful when the times aren't evenly spaced: 0 8,12,17 * * * runs at 8 AM, noon, and 5 PM.
Hyphen (-) specifies a range. 0 9-17 * * * runs at the top of every hour from 9 AM through 5 PM, inclusive on both ends. Ranges are inclusive, which catches some people who expect exclusive endpoints from other languages.
Slash (/) specifies a step. */5 * * * * runs every five minutes. The slash always comes after a range or asterisk: */5 is shorthand for "every fifth value across the full range," while 0-30/10 runs at minute 0, 10, 20, and 30. Most expressions in the wild use the asterisk form (*/N), but the range form is occasionally clearer when you want to constrain the window.
That is the entire alphabet. Asterisks, commas, hyphens, slashes, plus the numbers themselves. Every cron expression is built from those five things.
Photo by photoGraph on Pexels
Reading a few real expressions
The fastest way to internalize the syntax is to translate a handful of common patterns out loud.
0 * * * * means minute 0 of every hour. This is "once an hour, on the hour." Common for hourly metric rollups and cache warms.
*/15 * * * * runs every fifteen minutes. Useful for short-cadence health checks. Note that this fires at minutes 0, 15, 30, and 45, not "fifteen minutes after the last run." Cron is wall-clock driven, not interval driven.
0 2 * * * runs at 2:00 AM every day. The classic nightly batch slot, picked because most production systems are quietest in the small hours.
30 9 * * 1-5 runs at 9:30 AM, Monday through Friday. Business-hours weekday jobs typically look like this.
0 0 1 * * runs at midnight on the first of every month. Monthly reports, license renewals, retention sweeps.
*/15 9-17 * * 1-5 runs every fifteen minutes, between 9 AM and 5 PM, Monday through Friday. The compound expression from the opening of this article. It evaluates to about 144 runs a week.
If you find yourself second-guessing one of those translations, paste it into a free cron expression builder by EvvyTools and it will print the human-readable equivalent plus the next ten execution times. Verifying instead of guessing is the right reflex.
The day-of-month / day-of-week trap
The single most common cron mistake involves the two day fields. Most users assume that if both day-of-month and day-of-week are set, cron will only run when both match. That is wrong.
In standard cron, if day-of-month and day-of-week are both restricted (anything other than *), the job runs when either matches. This is an OR, not an AND.
0 0 15 * 1 does not mean "the 15th, but only if it's a Monday." It means "midnight on the 15th of the month, and also midnight every Monday." Roughly five times a month, not once a year.
The workaround depends on the implementation. On Linux systems using Vixie cron, you usually have to enforce the AND in the script itself: check the day of week inside the job and exit early if it doesn't match. Some modern cron implementations (notably the one in systemd.timer units) sidestep the problem entirely by using a different syntax. For long-form documentation, the Linux man pages describe the exact semantics for the GNU/Linux variant you are most likely to encounter.
"Every team I have worked with has shipped at least one cron job that fired on the wrong day because they assumed AND semantics on those two fields. The fix is cheap once you know the rule, but the silent miss in production can cost a billing cycle." - Dennis Traina, founder of 137Foundry
If you find yourself relying on the AND semantics often, that is usually a sign you should be reaching for a more expressive scheduler. Which brings us to the next section.
Photo by Svetlana Tumina on Pexels
Special characters cron does not have
A few patterns are tempting to write in cron syntax but are not part of the standard. Knowing what is missing keeps you from authoring expressions that look right and silently misbehave.
There is no built-in "last day of month" token in classic cron. 31 works for January, March, May, and so on, but in months with 30 days it simply never fires, and in February it has the same problem. Some forks (Quartz Scheduler, for example) accept L as last-day-of-month, but most Unix cron implementations do not.
There is no AM/PM modifier. Hours are 0-23, full stop. If you want 2 PM, you write 14.
There is no timezone field. The cron daemon interprets every expression in the system timezone (or the user's CRON_TZ if the implementation supports it). A job set to 0 9 * * * on a server configured to UTC will not match 9 AM local for the operator sitting in New York. Daylight saving transitions also produce edge cases: jobs scheduled during the missing hour are skipped, and jobs scheduled during the repeated hour may run twice. Run anything sensitive in UTC and convert at the application layer.
There is no built-in retry. If a cron job fails or the host is down at the scheduled minute, the run is lost. Cron will not catch up on missed runs unless you put a tool like anacron in front of it, which is a different daemon designed exactly for laptops and other intermittently-powered machines. The GNU mcron project is one alternative for cases where the standard semantics aren't enough.
How to debug a cron line that "should be working"
When a cron job is not firing the way you expect, the failure is almost always in one of four places.
The expression is wrong. Paste it into a parser before you accuse the system. A surprising fraction of "cron is broken" tickets are off-by-one errors in the minute or hour field. The cron-builder tool will show you the next ten execution times. If those don't include the time you expected, the expression is the bug.
The environment is wrong. Cron runs jobs in a minimal shell with almost no environment variables set. PATH is short. HOME may not be what you think. Scripts that work fine on your interactive shell can silently fail under cron because they call a binary by short name. Always use absolute paths in cron commands, or source a profile inside the script.
The output is hidden. By default, cron emails the job's stdout and stderr to the local user, and on modern hosts that often goes nowhere. Redirect explicitly: >> /var/log/myjob.log 2>&1. If you don't, you have no idea whether the job ran or what it printed.
The timezone is wrong. This is the same trap as the previous section but worth repeating. Confirm the daemon's view of "now" with date or, on systemd hosts, systemctl list-timers. If you and the daemon disagree on what time it is, the expression cannot save you.
For anything beyond simple cases, leaning on the existing tools at tools directory, including the visual builder, beats writing expressions by hand. The cost of a missed schedule is almost always higher than the cost of double-checking the syntax.
Photo by Enes Karahasan on Pexels
When cron is not the right answer
Cron is older than most of the engineers using it, and it shows in places. For jobs that need any of the following, reach for something purpose-built instead.
Distributed coordination. Cron has no notion of "this should run on exactly one host in a cluster." If you copy a crontab to ten machines, the job runs ten times. Modern alternatives like the scheduling primitives in Kubernetes (CronJob objects) and managed schedulers in the major cloud providers handle the locking for you.
Long-running tasks with retries. Cron fires the command and walks away. If you need exponential backoff, dead-letter queues, or visibility into in-flight runs, a job queue (Sidekiq, Celery, RQ, or one of the cloud-managed equivalents) is the right tool.
Sub-minute granularity. The minimum cron interval is one minute. If you genuinely need a job to fire every ten seconds, cron is not the right primitive; you need a long-running worker.
Anything where missing a run is unacceptable. Cron is fire-and-forget. If you can't tolerate lost runs from machine downtime, you need a scheduler with durability guarantees.
For the cases cron does handle well, namely periodic, single-host, minute-grained, idempotent jobs, it remains hard to beat for the line count it asks for. The full EvvyTools homepage lists adjacent calculators and decoders if you need to translate other compact syntaxes you run into.
Putting it together
Reading a cron expression is a small motor skill. Once you can name the five fields, recognize the four punctuation marks, and remember the day-of-month / day-of-week OR trap, the syntax becomes legible. The hard part is everything around the syntax: timezones, environments, missed runs, distributed coordination. None of those problems are visible in the five fields. All of them will eventually find you.
If you take one habit away from this guide, make it the verification reflex. Before any cron expression goes into a production crontab, run it through a parser, confirm the next handful of execution times, and read those times out loud. The five seconds of friction has saved more outages than any other practice in this space.