0 */2 * * * — is that every 2 hours, or every 2 minutes past the hour, or something else entirely? Cron syntax is five fields of dense shorthand, and it's easy to get right for a week and then quietly break the moment you need something slightly less common, like "the last day of the month" or "weekdays only, twice a day."
Here's what each field actually means, the patterns that cover almost every real use case, and the mistake that catches people most often.
The Five Fields
* * * * *
│ │ │ │ │
│ │ │ │ └── day of week (0-6, Sunday = 0)
│ │ │ └──── month (1-12)
│ │ └────── day of month (1-31)
│ └──────── hour (0-23)
└────────── minute (0-59)
Every cron expression is exactly five fields, left to right: minute, hour, day of month, month, day of week. A bare * in any field means "every value" for that field — * * * * * runs every single minute, which is almost never what you actually want.
Symbols You'll Actually Use
| Symbol | Meaning | Example |
|---|---|---|
* | Every value | * * * * * → every minute |
, | List of values | 0 9,17 * * * → 9am and 5pm |
- | Range of values | 0 9-17 * * * → every hour, 9am through 5pm |
/ | Step values | */15 * * * * → every 15 minutes |
0 (specific number) | Exact value | 30 2 * * * → 2:30am, once a day |
Common Schedules, Ready to Copy
- Every 5 minutes:
*/5 * * * * - Every hour, on the hour:
0 * * * * - Every day at 2:30am:
30 2 * * * - Every Monday at 9am:
0 9 * * 1 - Weekdays only, at 6pm:
0 18 * * 1-5 - Twice a day, 9am and 9pm:
0 9,21 * * * - First day of every month, midnight:
0 0 1 * * - Every 15 minutes during business hours (9am–5pm, weekdays):
*/15 9-17 * * 1-5
The Mistake That Catches People Most Often
Day-of-month and day-of-week are OR'd together, not AND'd, when both are set to something other than *. 0 0 15 * 1 doesn't mean "the 15th, if it's also a Monday" — it means "midnight on the 15th of the month, or every Monday at midnight," whichever comes first in a given week. If you only want one of those two conditions, leave the other field as *. This one behavior explains most "why did my cron job run on a day I didn't expect" bugs.
The second most common mistake is timezone assumptions — cron runs in the server's local timezone (or UTC, depending on how the scheduler is configured) unless you explicitly account for it. A job scheduled for "9am" on a server set to UTC will fire at a very different local time than you expect if you're not in that timezone. Always check what timezone the cron daemon or scheduler is actually running in before trusting an absolute time.
Why */2 Isn't Always What You Think
*/2 in the hour field means "every 2 hours, starting from hour 0" — so 0 */2 * * * fires at 12am, 2am, 4am, 6am, and so on, not "every 2 hours from whenever the job was created." Step values always anchor to the start of that field's range (0 for minutes/hours, 1 for day-of-month), not to the current time.
How to Check Your Expression Before You Deploy It
Rather than deploying a cron job and waiting to see if it fires at the right time, paste the expression into Cron Expression Generator — it validates the syntax and shows a human-readable explanation of exactly when it'll run, so you catch a misplaced field or a day-of-month/day-of-week conflict before it's live in production.
Quick FAQ
What does ? mean in some cron syntax?
? is a Quartz-scheduler-specific extension (used in Java/Spring schedulers), not standard Unix cron — it means "no specific value" for day-of-month or day-of-week, used specifically to avoid the OR conflict described above. Standard Unix crontab doesn't support ? at all — use * instead.
Can I run a job every second? No — standard cron's finest granularity is one minute. For sub-minute scheduling, you need an in-process timer or a scheduler built for that, not crontab.
What's the difference between cron and systemd timers?
Cron is the traditional Unix scheduler using this five-field syntax; systemd timers are a newer alternative on Linux with more flexible syntax (OnCalendar=) and better logging/dependency handling — but plain crontab syntax is still the most portable across environments, including most cloud schedulers (AWS EventBridge, GCP Cloud Scheduler, Kubernetes CronJobs) which use the same five-field format.
Why did my Kubernetes CronJob run twice?
Usually a concurrencyPolicy misconfiguration combined with a job that runs longer than the schedule interval — not a cron syntax issue. Check that setting if jobs overlap.
Related Free Tools
- Cron Expression Generator — validate a schedule and get a plain-English explanation
- Unix Timestamp Converter — convert between epoch time and human-readable dates
- JSON Formatter — validate config or job payloads alongside your schedule
- Regex Tester — for validating any pattern-matching logic in the same script