August 20, 2026 7 min read
Cron job timezone bugs and how monitoring catches them
Timezone misconfiguration is one of the most common causes of cron jobs running at the wrong time — or not running at all. Here's how to prevent it and how external monitoring catches it when prevention fails.
Timezone bugs in cron jobs share a common feature with silent failures: they produce no error. A cron job scheduled to run at 9am that actually runs at 1am doesn't crash. It doesn't throw an exception. It runs at the wrong time, completes successfully, and your monitoring tool records a healthy execution.
The damage accumulates in the gap between when the job ran and when it was supposed to run — a report generated before the business day's data is complete, a notification sent at 1am, a batch job that processes partial data because the window it was supposed to cover hasn't started yet.
This guide covers the most common timezone failure modes in production cron jobs, how to prevent them, and how external monitoring catches timezone problems that prevention misses.
How timezone bugs happen
Server timezone ≠ intended timezone. System cron reads schedules in the server's local timezone. If your server is configured for UTC and you write 0 9 * * * thinking it means 9am in your timezone, the job runs at 9am UTC — which may be 2am, 4am, or 5am in your actual timezone depending on where you are. This is the single most common timezone bug in production.
# Check your server's current timezone
timedatectl | grep "Time zone"
# Or:
cat /etc/timezone
If this shows UTC and you expected a local timezone, that's your problem.
Daylight saving time doubles or skips a run. Standard cron has no awareness of daylight saving time. During the "fall back" transition, when clocks move from 2:00am back to 1:00am, any job scheduled in the repeated hour (between 1:00am and 2:00am) runs twice. During the "spring forward" transition, when clocks skip from 2:00am directly to 3:00am, any job scheduled at 2:00am or 2:30am never runs at all — that time simply doesn't exist.
This is not an edge case. It happens twice a year in every timezone that observes DST, consistently, reliably, and with no error message.
Scheduler library timezone handling differs from system cron. Application-level schedulers (node-cron, APScheduler, Celery Beat) may use the server timezone, the application timezone, or a per-job timezone depending on configuration. The same schedule expression can behave differently across schedulers.
UTC offsets change seasonally. If you calculate your schedule as a UTC offset rather than using a named timezone, you have to remember to update it when DST changes. America/New_York is UTC-5 in winter and UTC-4 in summer. A schedule hard-coded as 0 14 * * * (2pm UTC to hit 9am EST) becomes wrong in March and stays wrong until November.
Prevention: the rules that eliminate most timezone bugs
Rule 1: Always run servers in UTC.
UTC does not observe daylight saving time. There is no spring-forward, no fall-back, no doubled or skipped hours. A server running in UTC and a cron expression using UTC offsets produces completely predictable behaviour year-round.
# Set server timezone to UTC (Ubuntu/Debian)
sudo timedatectl set-timezone UTC
# Verify
date
Rule 2: Use IANA timezone names, not abbreviations.
EST is ambiguous — it can mean UTC-5 (US Eastern Standard Time) or UTC+10 (Australian Eastern Standard Time). America/New_York is unambiguous. Always use IANA timezone identifiers.
Rule 3: For application schedulers, configure timezone explicitly per job.
Most scheduling libraries support per-job timezone configuration:
// node-cron — timezone per job
import cron from 'node-cron';
cron.schedule('0 9 * * *', sendDailyReport, {
timezone: 'America/New_York'
});
# APScheduler — timezone per job
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
import pytz
scheduler = BackgroundScheduler()
scheduler.add_job(
send_daily_report,
CronTrigger(hour=9, minute=0, timezone=pytz.timezone('America/New_York'))
)
Rule 4: Never schedule critical jobs between 1am and 3am.
This is the DST transition window. Any job scheduled in this window in a DST-observing timezone can run twice (fall back) or not at all (spring forward). For critical jobs, schedule outside this window as a matter of policy.
Rule 5: Document the intended local time alongside the cron expression.
# Runs at 9:00 AM America/New_York — currently 14:00 UTC (winter) / 13:00 UTC (summer)
0 14 * * 1-5 /usr/local/bin/send_report.sh
The comment is the only documentation the next engineer has for what this job is supposed to do in terms of business time. Without it, they have to reverse-engineer the UTC offset from context.
Where prevention fails and monitoring catches it
Prevention reduces timezone bugs. It does not eliminate them. A server timezone that was set correctly can be changed by a system update. A library upgrade can change default timezone handling. A deployment to a new region can put the job on a server in a different timezone. A DST transition can cause a double-run that wasn't caught in testing.
External monitoring catches timezone problems that code-level prevention misses — specifically by knowing when a job was supposed to run and alerting when the ping doesn't arrive in that window.
Scenario: server timezone changed during an OS update
The job was running correctly at 2am UTC (9pm US Eastern). An OS update changes the server timezone to US/Eastern. The job now runs at 2am US Eastern — 7am UTC. The six-hour difference won't surface as an error anywhere. External monitoring fires an alert at 2:30am UTC when no start ping arrives within the grace period after the expected time.
Scenario: DST spring-forward skips a run
The job is scheduled for 0 2 * * * in a timezone that observes DST. Spring-forward moves 2:00am directly to 3:00am. The job doesn't run. External monitoring fires an alert at 2:30am (the grace period) when no ping arrives.
Scenario: double-run during fall-back
The job runs at 1:30am, then the clock falls back and 1:30am happens again, and the job runs a second time. The overlap detection in Crontify fires — a start ping arrived while the previous run was still active — which surfaces the double-run as an alertable event rather than a silent duplicate execution.
Configuring monitors with timezone awareness
When creating a monitor in Crontify, configure:
- Expected schedule: the cron expression in UTC. Convert your intended local time to UTC before entering it. If you intend 9am New York time, enter
0 14 * * *in winter (UTC-5) or0 13 * * *in summer (UTC-4). - Grace period: how long after the expected time before a missed run alert fires. For jobs sensitive to timing, use a short grace period (5–15 minutes). For jobs where the exact time matters less, a longer grace period reduces false alarms.
For jobs with timezone-sensitive schedules, verify the next expected execution time in Crontify's dashboard matches your intended local time. If the dashboard shows the wrong time, your cron expression has a timezone error.
Frequently asked questions
Should I always use UTC for cron expressions?
For infrastructure jobs (backups, maintenance, data pipelines) — yes, always. UTC is stable, predictable, and unambiguous.
For business-logic jobs tied to business hours (send reports at 9am, process orders at market open) — use your application scheduler's timezone support rather than hard-coding UTC offsets. The scheduler handles the UTC conversion for you and automatically adjusts for DST.
How do I know if my job ran twice due to DST?
Overlap detection in your monitoring tool surfaces double-runs. The second execution's start ping arrives while the first run is still active, which triggers an overlap alert. Without monitoring, you'd have to check run logs or look for duplicate records to discover a double-run.
What's the safest schedule window for critical jobs?
3:00am to 23:00am local time, in a timezone that observes DST. This avoids the entire 1:00am–3:00am window where DST transitions occur. For global operations, scheduling in UTC avoids DST entirely.
Crontify is free for up to 5 monitors — no credit card required. Missed run detection fires when no ping arrives within your configured grace period — regardless of whether the cause is a timezone misconfiguration, a server crash, or a DST transition.
Start monitoring your scheduled jobs
Free plan includes 5 monitors. No credit card required. Up and running in under 5 minutes.
Get started free →More from the blog
August 20, 2026 6 min read
Why your cron job monitoring needs recovery alerts
Knowing when a job fails is half the picture. Knowing when it recovers completes it. Here's why recovery alerts matter, what happens without them, and how to configure them properly.
Read more →
August 20, 2026 6 min read
How to monitor Render cron jobs
Render sends failure alerts when a cron job exits non-zero — but no alert when a job never starts. Here's how to close that gap with external monitoring.
Read more →