TL;DR: Cron fields describe local clock patterns, but “local” belongs to the scheduler host—not your laptop. Confirm the daemon’s timezone, DST behavior, and whether the product uses five-field Unix cron or six-field variants before you paste an expression into a dev utility toolkit and call the job done.
The expression is only half the contract
0 9 * * 1-5 means “09:00 on weekdays” somewhere. On a UTC container it fires at 09:00 UTC. On a New York VM it fires at 09:00 America/New_York—except on the DST spring-forward day when 09:00 may be skipped or handled specially by the implementation. Teams page each other for “missed reports” that actually ran three hours early after a cloud region migrate.
Write the intended civil time in the ticket (“09:00 America/Chicago on business days”) and only then encode cron. If the platform cannot bind a timezone, run the host in UTC and convert consciously.
Five fields vs six fields vs macros
Classic Vixie cron uses minute hour day-of-month month day-of-week. Kubernetes CronJobs and some SaaS panels add a seconds field at the front. Quartz-style schedulers rearrange day-of-week numbering. A string that validates in one tester can be silently wrong in another.
- Check whether Sunday is 0, 7, or 1.
- Check whether day-of-month and day-of-week combine with AND or OR semantics.
- Prefer explicit lists over ambiguous shortcuts when the engine’s macro support is unclear.
Paste candidate strings into a parser that matches your production engine—not a random online dialect—via tools on www.masters-tool.com when you need a quick encode/decode pass beside your docs.
DST gaps and folds
Spring-forward removes an hour; fall-back repeats one. Schedulers differ: skip, run once, run twice. Finance jobs that must run exactly once should avoid the local ambiguous hour or pin to UTC. If marketing demands “9 AM local,” document what happens on transition Sundays and test with a temporary expression a week ahead.
UTC pinning as a team default
Many SRE groups store all cron in UTC and convert in the runbook. Humans still think in local time, so keep a one-line converter note next to every schedule. The cost is a small mental tax; the benefit is no silent seasonal drift.
Day-of-month versus day-of-week surprises
Some engines fire when either field matches; others require both. The infamous 0 0 13 * 5 style expression may mean “Fridays that are also the 13th” or “every Friday and every 13th” depending on software. Read the engine manual section—not a blog comment—before relying on that combo for payroll.
Container restarts and missed windows
Cron inside a single container only runs while the container lives. A deploy at 08:59 can skip a 09:00 job. Prefer platform CronJobs, systemd timers with Persistent=, or a queue scheduler that catches up. If you must use in-guest cron on a VPS, monitor last-success timestamps, not merely process uptime.
Validate with a next-run table
Good schedulers show the next five fire times. Screenshot them into the change ticket with the host timezone. If the panel cannot preview, temporarily point the job at a slack/webhook no-op and watch a dry-run day. Never discover timezone bugs on the first production invoice run.
Logging that makes audits possible
Log both UTC and intended local civil time when the job starts. Include the cron string and the resolved timezone ID. When someone asks “did it run at nine?” you answer with evidence instead of guessing which office’s nine they meant.
Human-facing “business hours” are not cron hours
Product managers say “every weekday at 9.” Engineers hear cron. Support agents hear “when the NY office opens,” which means 9 America/New_York including holidays exceptions cron will never know. Encode the civil timezone in the ticket, then maintain a holiday skip list in application code if needed. Trying to express “skip company holidays” purely in cron fields usually fails; keep cron boring and push calendar intelligence into the job body.
When stakeholders change “9” to “9:30,” update both the expression and the preview table. Half-hour shifts have broken more reporting SLAs than exotic day-of-week math.
Multi-region jobs and duplicate fires
Active-active schedulers in two regions can both honor the same cron and double-send invoices. Use leader election, a distributed lock, or a single scheduler region. Timezone correctness does not help if two healthy controllers both believe they are primary at 09:00:00.
Document which cluster owns cron. During failover drills, confirm the standby does not enable the same Crons until it takes leadership—or you will “successfully” page yourself with duplicate side effects.
A pre-schedule checklist for noisy teams
Before enabling any new expression: name the timezone, state DST policy, confirm field dialect, preview next runs, and decide catch-up behavior after downtime. Cron looks tiny—five punctuated numbers—but it encodes business promises. Treat those numbers with the same care you give API keys, and Monday morning will stop beginning with “why didn’t the job fire?”