Field Anatomy
┌──────────── minute (0-59)
│ ┌────────── hour (0-23)
│ │ ┌──────── day of month (1-31)
│ │ │ ┌────── month (1-12, or JAN-DEC)
│ │ │ │ ┌──── day of week (0-6, SUN-SAT, 0 and 7 = Sunday)
│ │ │ │ │
* * * * * command-to-runOperators
*— any value*/N— every N units (e.g.*/15in minutes = every 15 min)A-B— range (e.g.9-17in hours = 9 AM to 5 PM)A,B,C— list (e.g.1,15in day-of-month = 1st and 15th)A-B/N— range with step (e.g.0-30/10in minutes = 0, 10, 20, 30)
Common Patterns Worth Memorizing
# every minute
* * * * * # every 5 minutes
*/5 * * * * # every hour on the hour
0 * * * * # every day at midnight
0 0 * * * # every weekday (Mon-Fri) at 9 AM
0 9 * * 1-5 # every Sunday at 2:30 AM
30 2 * * 0 # 1st of every month at midnight
0 0 1 * * # every quarter (Jan, Apr, Jul, Oct) on the 1st
0 0 1 */3 * # every 15 minutes during business hours, weekdays
*/15 9-17 * * 1-5 # twice a day — 8 AM and 8 PM
0 8,20 * * *Special Shortcuts
Most cron implementations support these aliases — they're more readable than the equivalent expression:
@yearly/@annually—0 0 1 1 *@monthly—0 0 1 * *@weekly—0 0 * * 0@daily/@midnight—0 0 * * *@hourly—0 * * * *@reboot— once at system startup
The Gotchas That Bite Production
1. DOM + DOW = OR, not AND. If you write 0 0 1 * 1 expecting "the 1st of the month, but only if it's a Monday", you'll get every 1st AND every Monday. Pick one field and filter in your script if you need AND behavior.
2. Timezones. Cron uses the system timezone unless you set TZ= at the top of your crontab. Servers running UTC will fire 5 hours later than your local "midnight". Worse, on DST transition days, jobs scheduled between 2 AM and 3 AM either run twice or skip — which is why most teams run all schedules in UTC.
3. Output goes to the void. Cron emails stdout/stderr by default, but most servers don't have a working mail relay. Always redirect: 0 * * * * /usr/local/bin/job.sh >> /var/log/job.log 2>&1.
4. PATH is minimal. Cron jobs don't get your shell's full PATH. Either use absolute paths (/usr/bin/python3) or set PATH= at the top of your crontab.
Always Test Before You Commit
Pasting an expression into a cron parser before deploying takes 5 seconds and prevents the kind of "the report didn't run for two weeks and nobody noticed" incident that defines a bad week. Use the CoderFile cron parser to see the next 10 fire times for any expression — if those don't match your intent, your expression is wrong.
When to Outgrow Cron
Cron is great for one-off scripts on a single machine. For anything more — retries on failure, distributed scheduling, dependency chains, observability — use a real scheduler: systemd timers (per-machine, more flexible), GitHub Actions schedules (CI-managed), or a job system like Temporal/Airflow/Hatchet (production-grade).