All posts

Cron Job Time Explained: Clocks, Timezones, and DST Bugs

September 13, 2026

What "Time" Actually Means to Cron

A crontab line looks precise: five fields for minute, hour, day of month, month, and weekday, followed by the command to run. That precision is deceptive. The cron job time format doesn't encode an absolute moment — it's a pattern matched against whatever clock the cron daemon is reading when it wakes up to check its schedule.

The same crontab entry, 0 9 * * *, means something different depending on where it runs. On a server set to UTC, it fires at 09:00 UTC. On a server set to America/New_York, it fires at 9 a.m. Eastern — a different instant in absolute time. Cron time fields are relative to a clock, not a fixed reference point, and that single fact explains most "why did my cron job run at the wrong time" complaints. For a full breakdown of every field and syntax variant, the Cron Job Wiki is a good companion reference.

Which Clock Does Cron Read? (System Time vs UTC vs Local)

By default, cron uses the system's local timezone setting — not an inherently "correct" universal time, just whatever the operating system is configured to report. On most cloud instances (AWS, GCP, DigitalOcean, containerized deployments), that default happens to be UTC, which is why many engineers assume cron always runs in UTC. It doesn't. An on-prem server, a legacy VM, or a workstation configured with a regional timezone will have cron firing against that local clock instead.

You can check what clock your system — and therefore cron — is actually using with date or, on systemd-based Linux distributions, timedatectl. Both cron implementations you're likely to encounter, Vixie cron and its modern descendant cronie, inherit this system timezone unless told otherwise. This is confirmed in detail in crontabsheet.com's timezone handling guide: cron system time is the system's local time, full stop, and every scheduling decision downstream depends on getting that assumption right before you write a single crontab line.

The practical implication: two servers running identical crontabs can execute the same job at different absolute moments if their system clocks are set to different regions. If you manage a distributed fleet, or have inherited a server without knowing its timezone configuration, checking this first will save an hour of confused debugging later.

Overriding the Clock: CRON_TZ and TZ

Modern cron implementations let you override the default clock per crontab, or even per line, using the CRON_TZ variable. Set CRON_TZ=America/Chicago at the top of a crontab (or before a specific entry, depending on your cron version), and cron evaluates that schedule against Chicago time instead of the system default — using the IANA timezone database to resolve the offset and DST rules correctly.

TZ, by contrast, is a common source of confusion precisely because it looks like it should do the same thing. Setting TZ in a crontab does not change when the job fires — it only changes the timezone the spawned script's environment sees once it's already running. If your crontab reads:

TZ=America/Chicago
0 9 * * * /usr/bin/backup.sh

The job may still trigger at 9 a.m. server time (say, UTC), not 9 a.m. Chicago time — but any date command inside backup.sh will report Chicago local time. This CRON_TZ vs TZ distinction is exactly the gap that produces "I set the timezone and it still ran at the wrong time" tickets. If you need the trigger time itself to shift, CRON_TZ is the variable that does it; TZ only relabels the clock your script observes afterward.

Daylight Saving Time: When Cron Jobs Skip or Double-Run

Daylight saving time turns a solved scheduling problem back into an unsolved one, twice a year. During the "spring forward" transition, an hour of wall-clock time is skipped entirely — if your job is scheduled for 2:30 a.m. and the clock jumps from 2:00 to 3:00, that tick never occurs, and the job silently doesn't run that day. During "fall back," the reverse happens: an hour repeats, and a job scheduled inside that hour can fire twice.

This is a well-documented cron daylight saving time bug pattern, not a bug in cron itself but a structural consequence of scheduling against local time zones that observe DST. inventivehq.com's guide to handling timezones and DST in cron recommends avoiding jobs scheduled inside the 1–3 a.m. window in DST-observing zones, and — more durably — running cron in UTC wherever the job's business logic allows it. UTC never observes daylight saving time, so a job scheduled in UTC fires at the same absolute cadence year-round. If your job genuinely needs to align with a region's local business hours (a report that must land at "9 a.m. local"), CRON_TZ set to that region's IANA name will apply the correct DST rules automatically — but expect the absolute UTC trigger time to shift by an hour twice a year.

Scheduled Time vs Actual Run Time: The Gap Nobody Checks

Everything above assumes cron itself is alive, the host is up, and the tick you scheduled actually gets evaluated. That assumption fails more often than most teams realize. A host reboot during a maintenance window, a cron daemon that silently died, a container that restarted mid-cycle, a missed tick on an overloaded system — none of these produce an error. They just produce silence. Your crontab syntax can be flawless and your timezone configuration perfectly correct, and the job still doesn't run, because scheduled time and actual run time are two different things that nothing in a standard cron setup ever compares.

This gap is invisible by default because cron doesn't log a negative — it has no mechanism to tell you a job that should have fired, didn't. For a deeper look at what happens mechanically once a schedule is triggered, see what actually happens when cron runs. But knowing the mechanics doesn't close the gap — you need a record of actual execution timestamps to compare against expectation.

How to Know Your Cron Job Ran at the Right Time

Confirming a job fired when it should takes three things: a logged timestamp for every actual execution, an expected schedule to compare it against, and an alert when the two drift apart. In practice, that means recording not just "did the job succeed" but "when did it start," so a job that runs correctly but two hours late still gets flagged.

This is precisely the check that manual log-grepping fails to catch consistently, especially across distributed or cloud-hosted fleets where each host may have its own clock and cron configuration. Cronevra timestamps every real execution automatically, so you can see actual run time against scheduled time at a glance, catch DST-related misses the moment they happen, and get alerted when a job doesn't fire when expected — without writing your own comparison logic. For the deeper question of confirming a job actually ran and succeeded, not just fired, see Check Cron Job Status.

Correct crontab syntax and a properly configured CRON_TZ only guarantee that cron intends to run your job at the right moment — they say nothing about whether it actually did. That's the layer cron doesn't cover on its own. Cronevra closes it by timestamping every real execution, comparing it against the expected schedule, and alerting you the instant a job goes quiet — whether the cause is a DST transition, a dead daemon, or a rebooted host. Check the pricing page to see which plan fits your job volume.

Frequently Asked Questions

What time zone does a cron job run in by default?

Cron runs jobs against the system's local timezone, set at the OS level, not an inherently universal or "correct" time. On most cloud servers this default happens to be UTC, but on-prem and legacy systems may be configured to a regional timezone instead. Check your actual setting with date or timedatectl before assuming it's UTC.

How do I make a cron job run at a specific time in a different timezone?

Set the CRON_TZ variable in the crontab, either globally at the top of the file or before a specific job line, using an IANA timezone name like America/Chicago. This tells cron to evaluate that schedule's trigger time against the specified zone's clock and DST rules, rather than the system default.

What's the difference between CRON_TZ and TZ in crontab?

CRON_TZ changes when the job actually fires, because cron uses it to interpret the schedule itself. TZ only changes what timezone the job's environment sees once it's already running — it has no effect on the trigger time, which is why setting it alone doesn't fix a wrong-time job.

Why did my cron job run twice (or not at all) during a daylight saving time change?

During the "spring forward" transition, an hour of wall-clock time is skipped, so a job scheduled inside that hour never triggers that day. During "fall back," that hour repeats, so a job scheduled inside it can fire twice. Scheduling in UTC, which doesn't observe DST, avoids both failure modes entirely.

How can I tell if my cron job actually ran at its scheduled time?

You need a logged timestamp for every real execution, compared automatically against the expected schedule, with an alert on drift or a missed run. Cron itself doesn't log negatives — it can't tell you a job failed to fire — so tools like Cronevra that timestamp real executions and flag discrepancies are the only reliable way to confirm it.