All posts

Cron Job Time Zones: The DST Bug Hiding in Your Schedule

September 30, 2026

Why Time Zones Break Cron Schedules (Even When the Syntax Is Right)

A cron expression that parses correctly can still fire at the wrong time — or the wrong number of times — twice a year. Cron doesn't fail on syntax; it fails on the gap between wall-clock time and UTC, which drifts and jumps depending on where a server or container thinks it's located. Get cron job time zones wrong and you don't get an error — you get a job that ran twice, or never ran, with a perfectly clean execution log either way.

This is worse than it sounds because most teams run the same logical schedule across multiple regions, data centers, or a fleet of Kubernetes CronJob resources spread across clusters with different local clocks. Each instance observes its own region's daylight saving time transition on its own date. A schedule that's safe in one time zone can be actively dangerous in another, and a cron timezone setting that looks correct in code review can still produce a duplicate charge or a missing report in production.

The Two DST Failure Signatures: Duplicate Runs and Missing Runs

Daylight saving time creates exactly two failure modes, and both are silent.

Fall-back (duplicate runs). When clocks move back an hour — typically in autumn — one local hour repeats. A job scheduled for 1:30 AM local time will see that wall-clock moment occur twice in the same real-world night. Vixie cron and its descendants evaluate the schedule against local time, so if nothing accounts for the repeat, the job fires once during each pass through that hour. No crash, no retry, no anomaly in the logs — just a batch job, email send, or billing run that executed twice.

Spring-forward (missing runs). When clocks jump forward an hour, a local hour is skipped entirely. If your job is scheduled for 2:15 AM and that clock time never occurs that day, the job simply doesn't fire. Nothing logs an error, because there was nothing to error on — the trigger condition just never arose.

Both bugs cluster around the same danger window: roughly 1:00–3:00 AM local time, which most regions choose for their DST transition specifically because it's low-traffic. That's also exactly when teams schedule maintenance jobs, nightly batches, and reports — so the job runs twice, or doesn't run at all, and nobody notices until a downstream number looks off weeks later.

CRON_TZ, UTC, and Why 'Just Pin a Time Zone' Isn't Enough

The instinctive fix is to pin a time zone explicitly with CRON_TZ instead of relying on server local time. This helps with one real problem — it decouples your schedule from whatever time zone a host or container happens to be configured with, which matters when your infrastructure spans regions or gets rescheduled across nodes. But CRON_TZ doesn't eliminate the fold/skip bug; it just anchors it to a specific, chosen zone instead of an unpredictable one. Set CRON_TZ=America/New_York and schedule a job at 1:30 AM, and you still get the fall-back duplicate and the spring-forward gap — you've just made the failure deterministic and traceable to a zone you picked on purpose.

The alternative — schedule everything in UTC — removes DST ambiguity completely, since UTC never observes daylight saving time. That's genuinely valuable for reliability. The tradeoff is that a UTC-scheduled job's local meaning shifts by an hour twice a year. A report meant to land at "9 AM for the sales team" will actually land at 8 AM or 10 AM local time for part of the year unless something recalculates the UTC offset around each transition. UTC solves the cron DST bug technically but reopens it as a product or business logic problem — you've traded a scheduling defect for a communication one.

Designing Multi-Region Schedules That Don't Collide

For one logical job that needs to run at a consistent local time across several regions, the pattern that works has three parts.

First, store and schedule the canonical trigger in UTC. Let your scheduler — or a wrapper around it — compute the correct UTC offset for each target region using current IANA/tzdata rules, rather than hardcoding an offset that will be wrong the moment DST flips.

Second, treat "9 AM local" as a per-region calculation, not a single cron expression reused everywhere. New York, London, and Sydney don't shift their clocks on the same date, so the UTC time equal to 9 AM local in each region changes independently throughout the year. A shared CRON_TZ per job, one per region, recalculated against current zoneinfo data, is more reliable than one global expression with a fixed offset.

Third, actively avoid scheduling any instance inside the 1–3 AM window in any region you operate in — not just your primary one — and stagger jobs that touch shared downstream resources (databases, queues, third-party APIs) so a DST-shifted run in one region doesn't collide with a normally-timed run in another. This is the same discipline covered in more depth for retry logic, worth reading alongside this if your jobs touch payments or notifications.

Making Jobs Idempotent So a Double-Fire Can't Hurt You

Perfect scheduling design still won't survive every edge case, so the job itself needs a second line of defense: idempotency. Instead of keying a job's side effects to the wall-clock moment it was triggered, key them to the logical run date or period it represents — "invoices for 2024-11-03," not "the run that started at 1:47 AM." A fall-back duplicate fire then checks whether that logical key has already been processed and exits as a no-op instead of sending a second email or charge. This small implementation cost turns a DST fold from an incident into a non-event, and it pairs naturally with the retry and backoff strategies discussed in Cron Job Retry Strategy.

Monitoring for the Failure Modes You Can't Fully Design Away

Even with UTC scheduling, region-aware offsets, and idempotency keys, something outside your code can still break the schedule: a stale tzdata package on a container image that hasn't rebuilt in months, a Kubernetes node with a misconfigured system clock, or a government changing its DST rules with a few months' notice, as several countries have done in the past decade. None of these show up as application errors — they show up as an execution history with a gap or an unexpected cluster.

That's precisely the layer design can't cover, and monitoring has to. Watching for the two DST failure signatures — duplicate-run clusters and missing-run gaps — across every region a job serves is more reliable than trusting that your scheduling logic and every server's tzdata stayed correctly in sync. If you're defining reliability targets for scheduled work, it's also worth reading Cron Job SLA: A Practical Framework, and if your jobs run on managed serverless platforms, Serverless Cron Job Monitoring covers the timezone quirks specific to those environments.

You can design around DST perfectly and still get burned by a stale timezone database or a clock drift you never touched. Cronevra tracks execution history and sends recovery alerts the moment a job fires twice or not at all, so the fold and the skip get caught before a billing run or report goes silently wrong. Check pricing to set up monitoring on your multi-region schedules today.

Frequently Asked Questions

Does cron run a job twice during daylight saving time?

Yes, when the job is scheduled inside the hour that repeats during the fall-back transition. Cron evaluates local wall-clock time, so a job set for something like 1:30 AM will match that time twice on transition night and fire both times, with no error logged either time.

Should I schedule cron jobs in UTC or local time?

Schedule in UTC for the trigger mechanism, but compute region-specific local meaning separately using current IANA/tzdata rules. UTC removes DST ambiguity from the trigger itself, but a UTC time's local meaning still shifts by an hour twice a year, so anything user-facing needs that offset recalculated per region.

What happens to a cron job scheduled between 1 AM and 3 AM during a DST change?

It either fires twice (fall-back) or never fires (spring-forward), depending on the direction of the transition. This window is the highest-risk period in almost every DST-observing region because it's exactly when the clock change itself occurs.

How do I schedule the same job to run at 9 AM local time in five different regions?

Calculate the UTC-equivalent trigger time separately for each region using current zoneinfo data, rather than applying one fixed offset everywhere. Since regions shift their clocks on different dates, a single shared expression will drift out of sync with "9 AM local" for at least one region during part of the year.

Does CRON_TZ fix the daylight saving time problem?

No, CRON_TZ pins a job to a specific named zone instead of ambient server time, but the fold and skip behavior still occurs within that zone. It makes the failure deterministic and traceable rather than eliminating it.

Can an outdated tzdata package break my cron schedule even if my code is correct?

Yes. If a container or server's IANA/tzdata database hasn't been updated after a government changes its DST rules, the system will calculate the wrong local offset regardless of how correctly the scheduling logic was written, producing the same duplicate or missing run without any code defect.