Cron, timezones, and the 2 AM problem
"Run every day at 3 AM" sounds like the simplest schedule in the world. It isn't — not once Daylight Saving Time gets involved. This is the corner of the product I spent the most time on, so let me explain why.
Picture a job on 0 3 * * * — every day at three in the morning. Ninety-nine days
out of a hundred, "when does it next run?" has an obvious answer: tomorrow at 03:00. And then,
twice a year, the clocks move, and the obvious answer quietly becomes wrong.
The two weird nights
In spring, the clocks jump forward — 02:00 becomes 03:00, and the hour in between simply never happens. In autumn they fall back — 01:00 rolls around, then happens a second time. If your job runs on wall-clock time but your monitor is counting in tidy 24-hour chunks, the two drift apart by an hour. And they stay an hour apart for six months, until the next transition flips the disagreement the other way.
The practical result is the worst kind of bug: false alarms that only fire on the two nights a year when everyone is asleep, or — much worse — a genuine miss that slips by unnoticed because the monitor's idea of "on time" had quietly slid off by an hour.
I've been on the receiving end of this. There are few things less fun than being paged at 3 AM by a job that was, in fact, completely fine.
"Just use UTC" doesn't actually fix it
The usual advice is "store everything in UTC," and yes — Alwaysbeat does exactly that under the hood. But your schedule isn't a timestamp. It's a human intention: "3 AM local, after the day's data has landed." That intention is tied to a civil timezone, DST and all. Flatten it to a fixed UTC hour and you've silently broken it for half the year.
A schedule without an explicit timezone is a bug with a date already picked out for it.
So the schedule has to be evaluated in the zone you actually meant. There's no shortcut around it.
How Alwaysbeat handles it
Every cron check stores an explicit IANA timezone —
Australia/Melbourne, America/New_York, Europe/London. Your
cron expression is evaluated in that zone against a full timezone database, so a
0 3 * * * job fires at 3 AM local on both sides of a DST change: never skipped
in spring, never doubled in autumn.
kind: cron
cron_expr: 0 3 * * *
tz: Australia/Melbourne # evaluated here, DST and all
Because the deadline is worked out in the check's own zone, "next run" is always the one you meant — and your grace period sits on top of the correct time, not an hour to either side of it. I made timezone a required field on cron checks on purpose: I'd rather you tell me the zone than have the system guess and get it wrong at 3 AM.
What I'd suggest
- Name the timezone your job really lives in. Don't do the UTC conversion in your head — let the system evaluate the zone you named.
- Give DST-sensitive jobs a bit more grace. If something downstream also shuffles around the transition, a slightly wider grace period soaks up the wobble without hiding a real failure.
- If it's really "every N hours," use an interval. A job that genuinely runs every six hours, rather than at specific clock times, sidesteps the whole wall-clock question.
If you want the mechanics, the Schedules & grace page in the docs has the details.
— Anton
Your job pings a URL when it finishes. If a ping is late, we tell you.
Start monitoring — it's free →