Cron Expression FAQ — Complete Technical Reference
17 expert-reviewed answers covering cron syntax, platform differences, troubleshooting, and advanced scheduling patterns for DevOps engineers.
Fundamentals
A cron expression is a compact string notation that defines a time-based schedule for recurring task execution. Invented for Unix systems in the 1970s, it encodes "when to run" using space-separated fields representing time units (minute, hour, day, month, weekday). The cron daemon evaluates these fields every minute, and when the current system time matches all field constraints simultaneously, the associated command executes. Modern platforms like AWS EventBridge, Kubernetes, and Azure Functions use extended variants of this same syntax.
The number of fields determines what time units you can control and which platform the expression targets. Standard Unix/Linux cron uses 5 fields (minute, hour, day-of-month, month, day-of-week). AWS EventBridge uses 6 fields by adding a "year" field at the end. Spring Boot and Azure Functions use 6 fields by prepending a "seconds" field. Quartz Scheduler (Java) uses 6-7 fields with both seconds and an optional year. Using the wrong field count is the most common cause of cron misconfiguration in production deployments.
The asterisk (*) means "every value" in that field. The slash (/) defines step intervals — */5 in minutes means every 5 minutes (0, 5, 10...). The hyphen (-) defines ranges — 9-17 in hours means 9 AM through 5 PM. The comma (,) creates lists — 1,3,5 means those specific values. The question mark (?) means "no specific value" in AWS/Quartz day fields. L means "last" (last day of month or last specific weekday). W means "nearest weekday" to a given date. The hash (#) specifies the nth occurrence — 6#3 means "third Friday" (day 6 = Friday in Quartz).
Read cron expressions field by field from left to right. "0 9 * * 1-5" means: minute=0 (at the top of the hour), hour=9 (at 9 AM), day-of-month=* (any day), month=* (any month), day-of-week=1-5 (Monday through Friday). Combined: "At 9:00 AM, Monday through Friday." The key insight is that all fields must match simultaneously — the job runs only when the current time satisfies every field constraint at once.
Syntax Deep Dive
*/5 means "every 5th value starting from the field's minimum value." For minutes, this yields 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55. The notation 0/5 explicitly starts from 0, producing the same result for minutes. However, the difference matters in other contexts: 2/5 starts at 2, yielding 2, 7, 12, 17, 22... The star in */5 is shorthand for "start-end/5" using the full range. On AWS EventBridge, the behavior is identical, but some schedulers (like older versions of Quartz) may treat them differently when combined with ranges.
It depends on your platform. In standard Unix cron, if both day-of-month and day-of-week are specified (neither is *), the job runs when EITHER condition is true (OR logic). This surprises many developers. In Quartz and AWS EventBridge, you MUST set one of them to "?" — specifying both results in an error. This design difference has caused countless production incidents. Best practice: always set the unused day field to "?" on AWS/Quartz platforms, and document your intention with comments on Unix systems.
The L (Last) character is supported in AWS EventBridge, Quartz, and some extended cron implementations — but NOT in standard Unix crontab. In the day-of-month field, L means "the last day of the month" (28, 29, 30, or 31 depending on the month). You can also offset it: L-3 means "3 days before the last day." In the day-of-week field, xL means "the last x-day of the month" — e.g., 6L means "the last Friday of the month." This is invaluable for scheduling end-of-month reports or final-Friday deployments.
This is a critical compatibility issue. Unix/Linux cron uses 0=Sunday through 6=Saturday (with 7 as an alias for Sunday on many implementations). AWS EventBridge uses 1=Sunday through 7=Saturday. Quartz also uses 1=Sunday through 7=Saturday. Spring Boot uses 0=Sunday through 6=Saturday (same as Unix). The safest approach across all platforms is to use three-letter abbreviations (SUN, MON, TUE, WED, THU, FRI, SAT) which work universally and eliminate numbering confusion.
Platform-Specific
AWS EventBridge cron expressions use 6 fields: minutes, hours, day-of-month, month, day-of-week, year. The syntax is cron(fields). Key rules: (1) Always use "?" in either DOW or DOM, never "*" in both. (2) All times are UTC — no timezone support in the expression itself. (3) DOW uses 1=SUN through 7=SAT or three-letter names. (4) Supports L (last day), W (weekday nearest). (5) Year field supports 1970-2199 or "*". Example: cron(0 9 ? * MON-FRI *) = every weekday at 9 AM UTC. For rate-based schedules under 1 day, consider using rate() expressions instead.
Kubernetes CronJobs use standard 5-field Unix cron syntax in the .spec.schedule field. Before Kubernetes 1.25, schedules were evaluated against the kube-controller-manager's timezone (usually UTC). Since v1.25, you can set .spec.timeZone to any IANA timezone (e.g., "America/New_York"). Important settings: concurrencyPolicy (Allow/Forbid/Replace) controls overlapping executions, startingDeadlineSeconds sets how long K8s will try to start a missed job, and successfulJobsHistoryLimit controls how many completed pods are retained. Always set concurrencyPolicy to prevent runaway job accumulation.
Azure Functions uses NCRONTAB format: a 6-field expression with seconds as the first field (SEC MIN HOUR DOM MON DOW). Default timezone is UTC; override with the WEBSITE_TIME_ZONE application setting (use Windows timezone names on Windows hosts, IANA names on Linux). The expression goes in function.json's "schedule" binding or as a TimerTrigger attribute parameter. Azure supports a special %SettingName% syntax to reference app settings for the schedule value. Set "useMonitor": true to track missed executions, and "runOnStartup": true to fire immediately when the function app starts.
Troubleshooting
The top causes are: (1) Timezone mismatch — your server or cloud platform evaluates cron in UTC, but you wrote it in local time. (2) Wrong field count — you added a seconds field to a 5-field system or omitted it from a 6-field system, shifting all subsequent fields. (3) DST transitions — jobs scheduled between 2-3 AM may skip or double-fire during spring/fall transitions. (4) DOW numbering confusion — 1 means Monday in Unix but Sunday in AWS/Quartz. (5) OR vs AND logic — in Unix cron, specifying both DOM and DOW uses OR logic, which may run more often than expected. Always use the "Next 5 runs" preview to verify before deploying.
DST creates two edge cases: During "spring forward" (e.g., 2 AM → 3 AM), jobs scheduled in the skipped hour simply won't run. During "fall back" (e.g., 2 AM → 1 AM), jobs in the repeated hour may run twice. Solutions: (1) Schedule critical jobs outside 1-3 AM local time. (2) Use UTC-based scheduling to avoid DST entirely. (3) On Kubernetes 1.25+, setting .spec.timeZone handles DST correctly — jobs in the skipped hour fire at the next valid time. (4) For AWS EventBridge (UTC-only), DST isn't an issue, but you may need two rules for summer/winter if targeting local business hours.
Systematic debugging steps: (1) Verify the cron daemon/service is actually running (systemctl status cron, kubectl get cronjobs). (2) Check file permissions and PATH — cron runs with a minimal environment. (3) Confirm the schedule matches your expectation with a visualizer. (4) Check logs — /var/log/cron on Linux, kubectl describe cronjob on K8s. (5) On AWS, verify the EventBridge rule is ENABLED and has a target attached. (6) On K8s, check if startingDeadlineSeconds has elapsed or the job got suspended. (7) Verify your expression doesn't produce a "never" schedule — e.g., Feb 31 will never match.
Advanced Usage
Unix/Linux cron: 1 minute (cron daemon checks every 60 seconds). AWS EventBridge: 1 minute (for sub-minute use rate expressions or Step Functions). Kubernetes CronJobs: 1 minute (under 10 minutes the controller may behave unreliably). Spring @Scheduled: 1 second (supports seconds field). Azure Functions Timer: 1 second (but Consumption plan cold starts may add latency). Quartz: 1 second (in-process scheduler). For true sub-second scheduling, you need a different paradigm: message queues, event streams, or high-frequency polling loops.
This is one of the most requested patterns with no single-expression solution in standard cron. Approaches: (1) AWS EventBridge: Use "LW" in day-of-month — cron(0 17 LW * ? *) means "last weekday of every month at 5 PM". (2) Quartz: Same LW syntax in the DOM field. (3) Unix/Kubernetes: No native support. Schedule for days 28-31 and add logic in your script to check if it's the last weekday: [ $(date -d tomorrow +%m) != $(date +%m) ] or use the last 3 days and filter. (4) Alternative: Run daily and check programmatically if today is the last business day.
Best practices for cron validation: (1) Use a visual tool like Every Five to see the next N execution times in your target timezone. (2) Start with a high-frequency test expression (*/2 * * * *) in staging to verify the pipeline works. (3) Use dry-run modes: kubectl create cronjob --dry-run=client, or AWS EventBridge TestEventPattern API. (4) Implement idempotent jobs so double-firings during testing are safe. (5) Add monitoring/alerting for missed executions. (6) Use structured logging with expected_next_run timestamps to detect drift. (7) In CI/CD, validate cron syntax with libraries like croner (JS), croniter (Python), or cron-utils (Java) before deployment.