Why Your Cron Job Ran at the Wrong Time (Time Zones and UTC)

Suman SharmaSuman Sharma

You wrote 0 9 * * *, expecting a 9:00 job. It ran at 14:45, or 3:00, or an hour off after the clocks changed. The expression was fine. The problem is which clock the scheduler reads.

Step 1: Find the scheduler's time zone

Where it runsTime zone used
Linux crontabThe system time zone (timedatectl or /etc/localtime)
Docker containerUsually UTC unless you set TZ
GitHub Actions scheduleUTC
Vercel Cron JobsUTC
Kubernetes CronJobThe controller's zone (often UTC), or spec.timeZone if set
AWS EventBridge (classic rules)UTC

On a Linux server:

timedatectl # shows "Time zone: ..." date # local time date -u # UTC

If those two date outputs differ, cron is reading the first one.

Step 2: Convert your local time to UTC

Work out your offset from UTC and subtract it.

  • Nepal (UTC+5:45): 9:00 local = 03:15 UTC → 15 3 * * *
  • India (UTC+5:30): 9:00 local = 03:30 UTC → 30 3 * * *
  • London in winter (UTC+0): 9:00 local = 09:00 UTC → 0 9 * * *
  • New York in winter (UTC−5): 9:00 local = 14:00 UTC → 0 14 * * *

Watch the date line. 2:00 in Kathmandu is 20:15 the previous day in UTC. That matters if you also set a day of week: Monday 2:00 in Nepal is 15 20 * * 0, which is Sunday in UTC.

You can check the result in plain English with the Cron Expression Explainer.

Step 3: Set the time zone explicitly where you can

cronie (Fedora, RHEL, many others) supports CRON_TZ at the top of the crontab:

CRON_TZ=Asia/Kathmandu 0 9 * * * /usr/local/bin/report.sh

Debian's default cron does not reliably honour it, so test on your system before relying on it.

Kubernetes:

spec: schedule: "0 9 * * *" timeZone: "Asia/Kathmandu"

Docker: set TZ and install tzdata in the image if you want local time inside the container.

ENV TZ=Asia/Kathmandu

Hosted schedulers that only accept UTC: convert, and leave a comment with the local time so the next person doesn't "fix" it:

# 09:00 Asia/Kathmandu - cron: "15 3 * * *"

Step 4: Decide what to do about daylight saving

If your local zone uses daylight saving time (Europe, most of North America), a local-time schedule has two bad nights a year:

  • Clocks go forward: the 2:00–3:00 hour doesn't exist. A job at 2:30 may be skipped.
  • Clocks go back: the 1:00–2:00 hour happens twice. A job at 1:30 may run twice.

Your choices:

  1. Schedule in UTC and accept the job moves by an hour in local terms twice a year. Best for backups and data jobs.
  2. Schedule in local time but avoid the 1:00–3:00 window. Best for things people see, like a 9:00 report email.
  3. Make the job idempotent so a double run is harmless and a skipped run is caught next time.

Nepal and India don't observe daylight saving, so a UTC conversion there stays correct all year.

Other reasons it "ran at the wrong time"

  • It didn't run at all, and the next run looked late. Check logs: grep CRON /var/log/syslog or journalctl -u cron.
  • Hosted schedulers are best-effort. GitHub Actions scheduled workflows can start several minutes late at busy times.
  • The server clock is wrong. Make sure NTP is on: timedatectl set-ntp true.

Frequently asked questions

What time zone does cron use?

Classic cron uses the system time zone of the machine it runs on. Most hosted schedulers, including GitHub Actions and Vercel Cron Jobs, interpret schedules in UTC.

How do I run a cron job at 9 AM in my local time?

Either set the time zone explicitly where the scheduler supports it (CRON_TZ in cronie, timeZone in Kubernetes CronJobs) or convert 9 AM local time to UTC and schedule that hour instead.

Why did my cron job run twice or not at all?

Daylight saving. When clocks go back, a local hour repeats and a job in that hour can run twice. When clocks go forward, an hour is skipped and a job in it may not run. Scheduling in UTC avoids both.

Contact

Get in Touch

Want to call me? book a call and I'll respond in the available time slot. I will ignore all soliciting.