Cron Syntax Breakdown
The standard cron expression consists of five space-separated fields that define when a job executes: minute hour day-of-month month day-of-week. Each field accepts numeric values within specific ranges, wildcards, lists, ranges, and step operators.
Field ranges are strictly defined: minutes run from 0 to 59, hours from 0 to 23 (24-hour clock), day of month from 1 to 31, month from 1 to 12 (January through December), and day of week from 0 to 7 where both 0 and 7 represent Sunday—most systems accept either, though 0 is more common. Day-of-week naming varies slightly between implementations but the numeric values remain consistent.
A basic example: 0 0 * * * runs daily at midnight (00:00). */5 * * * * runs every 5 minutes. 0 2 * * * runs at 02:00 every day—a common time for database backups because traffic is low.
Some cron implementations support a sixth field for year (rarely used in production) or allow named months and weekdays like JAN, MON. Linux and macOS cron use the five-field standard. AWS EventBridge extends to six fields. Kubernetes CronJobs follow the standard five but add timezone configuration options. Always check your platform documentation before deploying to production.
Special Characters and Operators
The asterisk (*) is a wildcard meaning "all possible values." In the minute field, * means every minute from 0 through 59.
The comma (,) creates value lists. 0 0,12 * * * runs at midnight and noon. 0 9 * * 1,5 runs at 09:00 on Monday and Friday—useful for start-of-week and end-of-week reports.
The hyphen (-) specifies ranges. 0 9-17 * * * runs every hour from 09:00 through 17:00 (business hours monitoring). Ranges are inclusive on both ends.
The slash (/) defines step values. */15 * * * * runs every 15 minutes. 0 */2 * * * runs every 2 hours at minute zero. Step notation works with ranges: 5-45/10 means 5, 15, 25, 35, 45.
The question mark (?) appears in some enterprise schedulers (Quartz, Spring) as an alternative to * when you want to ignore a field—usually for day-of-month or day-of-week to avoid conflicts. Standard Unix cron does not recognize it.
The letter L ("last") is supported by AWS EventBridge and enterprise systems to mean the last day of the month or last occurrence of a weekday: 0 0 L * * runs on the last day of each month. Not available in standard crontab.
The letter W ("weekday") finds the nearest weekday to a given date in some implementations. 0 0 15W * * runs on the weekday closest to the 15th. Again, not standard—check your platform.
The hash (#) specifies the nth occurrence of a weekday within a month: 0 0 * * 5#2 runs on the second Friday of every month. Useful for monthly reports that must fall on specific weekdays. Supported by AWS EventBridge and Quartz, not by Unix cron.
Common Patterns and Real Examples
Production-tested cron expressions with real-world use cases:
0 2 * * *Daily database backup at 02:00 (low traffic window, less contention on disk I/O)
0 */1 * * *Hourly log rotation and compression to prevent disk fills
0 9 * * 1Weekly report generation every Monday at 09:00 for management review
*/10 * * * *Health check every 10 minutes to ping external dependencies and alert on failure
0 3 * * 0Weekly cache clearing every Sunday at 03:00 to reset application state
0 4 1 * *Monthly database maintenance on the first day of the month at 04:00 (VACUUM, ANALYZE, index rebuilds)
0 0 * * *Certificate renewal checks at midnight to catch expirations before they break production
*/5 9-17 * * 1-5Business hours monitoring every 5 minutes, weekdays only (09:00–17:00, Mon–Fri)
30 5 * * *Daily data sync at 05:30 before business day starts
0 0 1 1 *Annual archive job every January 1st at midnight to move old data to cold storage
Timezone Considerations and Pitfalls
Cron runs in the server's local timezone by default. If your server is configured to US Eastern time and you schedule a job for 03:00, it runs at 03:00 Eastern—not UTC, not your local development machine time. This is the single most common source of confusion and late-night debugging sessions.
Daylight saving time causes jobs to run twice (when clocks fall back) or skip entirely (when clocks spring forward). A job scheduled for 02:30 in a DST-observing timezone will execute twice on the fall transition and not at all on the spring transition because 02:30 doesn't exist that day. If your job must run exactly once per day, schedule it outside DST transition hours (03:00 or later is safer).
AWS EventBridge and many cloud schedulers operate exclusively in UTC. If you write 0 9 * * * expecting 09:00 in your local time, it will run at 09:00 UTC instead—possibly the middle of the night for you. Always convert your desired local time to UTC before deploying.
Kubernetes CronJobs support a spec.timeZone field (as of v1.25+) so you can specify America/New_York or Europe/Berlin explicitly. Without it, CronJobs use the timezone of the kube-controller-manager, which is usually UTC in managed clusters.
In traditional crontab, you can set the TZ environment variable at the top of the file: TZ=America/Los_Angeles on its own line before your cron entries. This forces all subsequent jobs in that crontab to use the specified timezone.
The debugging process for "why did my job run at the wrong time" goes like this: check date or timedatectl on the server to see its timezone. Check cron logs for actual execution timestamps. Confirm whether your platform uses local time or UTC. Adjust your expression accordingly. Document the timezone assumption in a comment so the next person doesn't waste an hour.
Testing and Validation
Test your cron expression before deploying it to production. Period. A syntax error or logic mistake can cause missed backups, failed deployments, or silent data loss.
Use online cron validators like crontab.guru or this tool to verify your expression and see the next 5–10 execution times. If the schedule doesn't match your expectation, fix it now rather than discovering the problem at 3am when backups didn't run.
Check cron logs after deployment. On most Linux systems, cron output goes to /var/log/cron or /var/log/syslog. Use journalctl -u cron on systemd systems. AWS CloudWatch Logs captures EventBridge executions. Kubernetes logs are accessible via kubectl logs on the CronJob's pods.
For critical jobs, run a dry-run or test execution manually before trusting the schedule. Execute the script with the same environment and permissions that cron will use. Cron jobs often fail due to missing PATH variables or insufficient file permissions that work fine when you run the script interactively.
Common validation mistakes: forgetting that day-of-month and day-of-week are logically OR'ed (not AND'ed), mixing up field order (easy to swap hour and minute), assuming ranges are zero-indexed when months are 1-indexed, forgetting timezone differences between dev and prod environments.
Monitor cron job execution with alerting. If a backup job hasn't run in 25 hours, something is broken. Set up simple checks that fire alerts when expected jobs don't write logs or update timestamps within their scheduled window.
Common Mistakes and How to Avoid Them
Real errors I've seen in production and how they broke things:
Confusing day-of-month with day-of-week logic
If you specify both day-of-month and day-of-week (neither is *), cron uses OR logic, not AND. 0 0 13 * 5 runs on the 13th of every month AND every Friday—not just Friday the 13th. This is counterintuitive. The fix: set one field to * if you only care about one dimension.
Wrong field order
Swapping minute and hour is common. 30 2 * * * runs at 02:30. 2 30 * * * runs at 30:02 which is invalid and won't execute at all. Always double-check: minute comes first.
Zero-based vs one-based indexing
Months are 1-indexed (1=January, 12=December) but day-of-week is 0-indexed (0=Sunday, 6=Saturday, with 7 also meaning Sunday for compatibility). Mixing these up means your monthly job runs in the wrong month or your Sunday job runs on Monday.
Timezone assumptions
Deploying a cron expression written in your local timezone to a UTC server results in jobs running at the wrong time. Always know your target platform timezone and convert your desired schedule accordingly.
Overlapping schedules causing resource contention
Scheduling two heavy database jobs at the same time (both at 02:00) can cause locks, slow queries, and timeouts. Stagger jobs by a few minutes: 02:00, 02:15, 02:30.
Forgetting to handle job failures
Cron executes the job and moves on. If your script fails silently, cron doesn't retry or alert you. Redirect output to a log file and monitor it: 0 2 * * * /path/to/script.sh >> /var/log/myjob.log 2>&1
PATH and environment issues
Cron runs with a minimal environment. Commands that work in your shell might not work in cron because $PATH is limited. Use absolute paths to binaries or set PATH at the top of your crontab: PATH=/usr/local/bin:/usr/bin:/bin
The 0 0 1 * * mistake from the opening
This runs on the 1st of every month at midnight—not daily. The developer wanted 0 0 * * * (asterisk in day-of-month). The database filled up for 29 days before anyone noticed. This is why you preview your expression before deployment.
Platform-Specific Implementations
Standard Unix/Linux cron
Five-field syntax. Runs in server local time. Edit with crontab -e. Logs to /var/log/cron or syslog.
AWS EventBridge
Six-field syntax with seconds. Always UTC. Supports L, W, # operators. See AWS EventBridge schedule expressions.
Kubernetes CronJobs
Five-field standard syntax. Supports spec.timeZone (v1.25+). See Kubernetes CronJobs.
GitHub Actions
Five-field POSIX syntax. Always UTC. Uses on: schedule: in workflow YAML. See GitHub Actions schedule.
Jenkins
Five-field with extensions. Supports H (hash) for distributed load. Timezone depends on Jenkins server config. See Jenkins cron syntax.
Azure DevOps
Uses a different YAML-based scheduling syntax, not cron expressions. See Azure scheduled triggers.
FAQ
What's the difference between 0 0 * * * and 0 0 * * 0?
0 0 * * * runs daily at midnight. 0 0 * * 0 runs only on Sundays at midnight because day-of-week is set to 0 (Sunday).
Can I run a job every 90 minutes?
Standard cron cannot express 90 minutes directly. Run it every hour and write logic inside the script to check if 90 minutes have elapsed since last run, or use a scheduling system that supports intervals like systemd timers.
Why isn't my cron job executing?
Check /var/log/cron or journalctl -u cron. Common issues: wrong syntax, PATH not set in crontab, script not executable, or timezone mismatches. Test your expression with this tool first.