Cron Jobs, Time Zones, and Daylight Saving Time
Design recurring jobs around explicit time zones, daylight-saving transitions, missed runs, duplicate execution, and the difference between elapsed time and wall time.
“Run every day at 2:30” is incomplete until you name a time zone and decide what happens when local clocks skip or repeat that time. Cron syntax describes calendar matches; it does not automatically define the business meaning of a schedule. Jobs involving billing, reports, reminders, or data windows need an explicit policy for time.
Use the utilkit Cron Expression Tool to build and inspect the expression, then test it in the scheduler that will execute it. Cron dialects differ in field count, special operators, timezone support, and missed-run behavior. Copying an expression between systems without checking the dialect can change its meaning.
Choose wall time or elapsed time
A wall-time requirement follows a local clock: “send at 9:00 a.m. Chicago time.” An elapsed-time requirement follows intervals: “run every 24 hours after completion.” These are not equivalent across daylight-saving changes. Write the intended behavior in plain language before choosing cron.
For global systems, UTC avoids daylight-saving transitions, but it does not keep a task at the same local hour year-round. If users expect 9:00 a.m. local time, store an IANA zone such as America/Chicago rather than a fixed offset. The IANA Time Zone Database records historical and current regional rules.
Plan for missing and repeated local times
During a spring-forward transition, some local times never occur. During a fall-back transition, an hour occurs twice. Traditional cron implementations may skip or repeat jobs in these periods; specific behavior varies. The crontab manual page explicitly warns that nonexistent times will not match and repeated times may cause a job to run twice.
Decide whether a skipped occurrence should run late, be omitted, or trigger an alert. Decide whether repeated matches should both run. For money movement and notifications, “twice” is rarely acceptable.
Make jobs idempotent
A scheduler can deliver the same trigger more than once because of retries, failover, or operator action even when daylight saving is irrelevant. Give each logical run a stable key, such as report type plus business date, and make the job safe to retry. Record start, completion, outcome, and the covered data interval.
Separate selection time from execution time. A daily report should define whether it covers the prior calendar day in a user’s zone, the last 24 hours, or data since the previous successful checkpoint. Those windows produce different records around clock changes.
Test the schedule as data
- Generate upcoming run times across both annual clock transitions.
- Verify scheduler and application time zones explicitly.
- Simulate a missed run, retry, and overlapping execution.
- Use a lock or uniqueness key when concurrent runs would conflict.
- Alert on absence as well as failure.
Write a job contract beside the schedule
Document what one run owns: its input window, time zone, identifier, expected outputs, maximum duration, retry policy, and alert threshold. For a daily report, “process yesterday” is ambiguous around deployments and time zones. A clearer contract is “process events from the previous closed America/Chicago calendar day,” with the computed start and end included in logs.
Use a durable run key based on the logical window, not only the current timestamp. Before writing output, check whether that key already completed; when updating many records, use checkpoints or a transaction appropriate to the workload. If a run fails halfway, the next attempt should resume or repeat safely. A lock may prevent overlap, but it should expire or be recoverable after a crashed worker.
Test the contract with the spring-forward date, fall-back date, month and year boundaries, leap day, and a delayed run that starts after its nominal time. Monitor both failure and absence. A job that never starts may emit no application error, so an external expectation such as “one completed run key by 08:00” is often more valuable than an alert inside the job itself.
Separate scheduling from business logic so the same job can be run safely for an explicit window from a command line or administrative tool. That makes missed-run recovery and testing possible without editing the cron expression or pretending the current time is different. Restrict manual execution, record who requested it and which window it covered, and use the same idempotency controls as scheduled runs. Operations become much calmer when “run yesterday again” is a supported, auditable action.
Decide who owns holidays and exceptional calendars. A weekday schedule does not know company closures, market calendars, regional holidays, or an upstream system’s availability. If the job should skip or shift, model that business calendar explicitly and test the change before the date arrives. If it should still run, ensure on-call coverage and dependencies match. Calendar exceptions are operational requirements, not clever cron expressions, and they deserve the same review as other input data.
Cron is good at expressing recurring calendar matches. Reliability comes from the surrounding design: a named time zone, an explicit data window, idempotent work, and observable outcomes. Once those decisions are documented, the five fields are usually the easy part.