How Cron Handles Timezones — A Practical Guide
"It worked fine in staging" is a phrase that shows up a lot in timezone bugs, because a cron expression has no idea what timezone it's supposed to mean — it just gets interpreted against whatever clock the scheduler is using. For a team spread across India and servers hosted in US or EU regions, this is one of the most common sources of "why did the job run at 3:30 AM" surprises. If you need the syntax basics first, start with our cron expression complete guide — this one assumes you already know the five fields and focuses purely on the clock they're evaluated against.
The core rule: cron has no built-in timezone awareness
A cron expression like 0 9 * * * just means "when the hour field equals 9 and the minute field equals 0" — according to whatever clock the cron daemon reads. On a Linux server, that's the system's configured local timezone by default. Check it with:
$ timedatectl
Local time: Fri 2026-07-11 14:32:07 IST
Universal time: Fri 2026-07-11 09:02:07 UTC
Time zone: Asia/Kolkata (IST, +0530)
# If a job says "0 9 * * *" on this server, it fires at
# 9:00 AM IST, which is 3:30 AM UTC.Setting a specific timezone for cron jobs
Most modern cron implementations (Vixie cron, cronie, and the cron used in Debian/ Ubuntu/RHEL) support a CRON_TZ variable that overrides the timezone for entries below it, without changing the whole server's timezone:
# This entry always fires at 9:00 AM IST, regardless of server timezone CRON_TZ=Asia/Kolkata 0 9 * * * /opt/scripts/daily-report.sh # A later entry in the same crontab, unaffected, runs in UTC CRON_TZ=UTC 0 0 * * * /opt/scripts/utc-midnight-job.sh
This is safer than relying on the server's system timezone, because it survives a server migration, a container rebuild, or an ops team changing the default OS timezone for unrelated reasons.
The classic failure: server timezone changes silently
- A server gets rebuilt from a base image that defaults to UTC instead of the previous IST configuration — every "9 AM" job now fires at 2:30 PM.
- An app migrates from a self-managed VM to a managed container platform, whose containers default to UTC regardless of the previous host timezone.
- Daylight saving time shifts a job's real-world time by an hour on regions that observe DST (India does not observe DST, but many US/EU teams working with Indian teams do, causing coordination confusion twice a year).
The fix is always the same: don't depend on "whatever the server happens to be set to." Pin the timezone explicitly with CRON_TZ, or better, run everything in UTC and do timezone conversion in application code where it's testable.
How cloud schedulers handle timezone
- GitHub Actions — scheduled workflows always run in UTC, with no timezone option in the YAML. A 9:00 AM IST report needs
cron: '30 3 * * *'(3:30 AM UTC). - AWS EventBridge Scheduler — lets you set an explicit timezone per schedule (unlike the older EventBridge Rules cron, which is UTC-only), so you can specify
Asia/Kolkatadirectly and it handles DST-aware regions correctly. - AWS EventBridge Rules (classic) — cron and rate expressions are always evaluated in UTC; you must convert manually.
- GCP Cloud Scheduler — accepts an explicit timezone parameter per job, similar to EventBridge Scheduler.
- Kubernetes CronJob — historically used the controller's local timezone; recent Kubernetes versions support an explicit
timeZonefield on the CronJob spec.
In-process schedulers handle it the same way, just with a code-level parameter instead of a platform setting: Node's node-cron accepts a timezone option directly (see cron vs setInterval in Node.js for how that's wired up), and Python's APScheduler accepts the same idea via BackgroundScheduler(timezone=...) — covered in cron jobs with Python's schedule library and APScheduler.
Practical recommendations
- Prefer running servers in UTC and setting
CRON_TZonly for jobs that must align with a business-facing local time (e.g. "send report at 9 AM IST to the Mumbai team"). - Document the intended timezone directly as a comment next to every crontab entry — future you (and teammates) will thank you.
- For cloud schedulers without timezone support (GitHub Actions, classic EventBridge), always write the UTC-converted time and add a comment showing the IST equivalent.
- Test timezone-sensitive jobs around DST transition dates if any part of your team or infrastructure observes DST — even though India doesn't, US/EU-hosted infrastructure often does.
Common Timezone Mistakes
- Assuming crontab times are already in UTC. Most Linux installs default to the system's local timezone, not UTC — check with
timedatectlbefore you assume, especially on a freshly provisioned server. - Placing CRON_TZ below the jobs it's meant to affect.
CRON_TZonly applies to crontab lines that come after it in the same file — putting it at the bottom, or after the job it's supposed to control, silently does nothing. - Hardcoding a UTC offset instead of an IANA zone name. Using
+05:30instead ofAsia/Kolkatalooks equivalent today, but offsets don't auto-adjust for DST-observing regions — a named zone does. - Testing a timezone-sensitive job only once, outside DST season. A job that fires correctly in July can silently shift an hour after a DST transition in November or March on any DST-observing infrastructure it touches.
- Assuming every cloud scheduler behaves the same way. GitHub Actions and classic AWS EventBridge Rules are UTC-only; EventBridge Scheduler and GCP Cloud Scheduler accept an explicit timezone. Mixing these up produces an off-by-N-hours bug that's easy to miss in code review.
- Not re-checking timezone after a platform migration. Moving from a self-managed VM to a managed container platform often silently resets the default timezone to UTC, shifting every "local time" job that didn't pin its timezone explicitly.
Putting It Together: A Multi-Region Schedule
A more realistic case than a single crontab line: a SaaS product with an India-facing report and a UTC-based backup, both defined explicitly so neither depends on whatever timezone the server happens to default to:
# /etc/cron.d/reports — always explicit, never relies on server default # 9:00 AM IST daily sales report for the Mumbai team CRON_TZ=Asia/Kolkata 0 9 * * * app /opt/scripts/daily-sales-report.sh # Midnight UTC backup — independent of any region's business hours CRON_TZ=UTC 0 0 * * * app /opt/scripts/nightly-backup.sh
The same explicitness applies to a Kubernetes CronJob, where recent versions support a native timeZone field instead of relying on the controller's local clock:
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-sales-report
spec:
schedule: "0 9 * * *"
timeZone: "Asia/Kolkata"
jobTemplate:
spec:
template:
spec:
containers:
- name: report
image: myorg/daily-sales-report:latest
restartPolicy: OnFailureBoth examples make the timezone a first-class, visible part of the schedule instead of an assumption baked into the host. If you also need to tune how often each of these fires — every 5 minutes, every 30 minutes, or hourly — see our cron expression examples for every 5, 10, 15, and 30 minutes, or build the base expression with the free Cron Expression Generator and then add the timezone yourself.
Frequently Asked Questions
Standard Linux cron uses the system's local timezone by default, not UTC, unless the CRON_TZ environment variable is set or the server itself is configured to run in UTC. This differs from many cloud schedulers, which default to UTC.
GitHub Actions scheduled workflows always run in UTC. There is no way to set a different timezone in the workflow YAML, so you must calculate the UTC-equivalent time yourself, for example 9:00 AM IST becomes 3:30 AM UTC.
Yes, on systems using Vixie cron or cronie you can set CRON_TZ=Asia/Kolkata on its own line directly above a crontab entry, and that line will be evaluated in the specified timezone regardless of the system default.
IST (Asia/Kolkata) is UTC+5:30 year-round with no DST, so subtract 5 hours and 30 minutes: 9:00 AM IST becomes 3:30 AM UTC, written as 30 3 * * * for a UTC-only scheduler like GitHub Actions or classic AWS EventBridge Rules.
It affects any job scheduled with a named timezone that observes DST, such as America/New_York or Europe/London — the job's real-world trigger time shifts by an hour twice a year. India (Asia/Kolkata) does not observe DST, so IST-only schedules are unaffected, but US- or EU-hosted infrastructure and cross-timezone coordination with Indian teams often are.