Back to Blog
Troubleshooting11 min read

Cron Job Timezone Issues: How to Fix Common Problems

Your cron job ran at 3 AM instead of 9 AM. Or it ran twice when the clocks changed. Or it skipped a day entirely. Timezone issues are the single most common source of cron-related bugs, and they are entirely preventable once you understand how the system works.

How Cron Determines the Time

Traditional cron (Vixie cron) uses the system timezone of the server it runs on. There is no timezone field in a cron expression. When you write 0 9 * * *, cron interprets "9" as hour 9 in whatever timezone the server's clock is set to.

Check your server timezone with:

# Show the current system timezone
timedatectl
# or
cat /etc/timezone
# or
date +%Z

If your server is set to America/New_York and you schedule a job for 0 9 * * *, it runs at 9 AM Eastern. If you move that crontab to a server set to Europe/London, the same expression now runs at 9 AM GMT/BST. Same expression, different actual time.

Common trap

Cloud providers often set servers to UTC by default. If you assumed your server runs in your local timezone, every cron schedule will be off by the UTC offset. A job you intended to run at 9 AM EST actually runs at 9 AM UTC (4 AM EST in winter, 5 AM EDT in summer).

Using CRON_TZ to Set Per-Crontab Timezones

Some cron implementations (notably the version in most modern Linux distributions) support the CRON_TZ variable. Set it at the top of your crontab file and all subsequent entries use that timezone:

# crontab -e
CRON_TZ=America/Chicago

# This runs at 9 AM Central, regardless of server timezone
0 9 * * * /usr/bin/php /var/www/app/send_report.php

# This runs at 2:30 AM Central
30 2 * * * /usr/bin/python3 /opt/scripts/backup.py

Important caveats:

  • CRON_TZ is not part of the POSIX standard. It works on most Linux distributions with Vixie cron or cronie, but not on all systems.
  • macOS launchd does not support CRON_TZ.
  • Some shared hosting environments strip environment variables from crontab files.
  • The timezone name must match the system's tz database (e.g., America/New_York, not EST). Three-letter abbreviations are ambiguous and unreliable.

An alternative that works everywhere is to use the TZ environment variable in the command itself:

# Set timezone only for this specific command
0 9 * * * TZ=America/Chicago /usr/bin/php /var/www/app/report.php

Note: this sets the TZ for the command's execution environment, but cron still evaluates the schedule in the system timezone. It affects when the script thinks it is running, not when cron actually triggers it.

The Daylight Saving Time Problem

DST transitions are where timezone issues go from annoying to genuinely dangerous. Here is what happens:

Spring Forward (Clocks Jump Ahead)

In most of the US, clocks jump from 2:00 AM to 3:00 AM on the second Sunday of March. The hour between 2:00 and 2:59 does not exist. If you have a cron job scheduled for 2:30 AM:

Cron ImplementationBehavior
Vixie cronJob is skipped entirely. It does not run that day.
cronie (RHEL/CentOS)Job runs at 3:00 AM (the first valid time after the gap).
systemd timersJob runs immediately when the timer notices it was missed.

Fall Back (Clocks Repeat an Hour)

Clocks go from 2:00 AM back to 1:00 AM on the first Sunday of November. The hour between 1:00 and 1:59 happens twice. A job scheduled for 1:30 AM may run twice, or it may run once in the first occurrence and be skipped in the second, depending on the implementation.

Real-world impact

A billing system that charges customers at 1:30 AM could charge them twice during the fall-back transition. A database backup scheduled at 2:30 AM could be skipped entirely during the spring-forward transition. Both are scenarios that have caused production incidents at companies of all sizes.

The Case for Running Everything in UTC

The simplest way to eliminate DST issues entirely is to set your servers to UTC and schedule all cron jobs in UTC. UTC does not observe daylight saving time. There are no skipped hours, no repeated hours, and no ambiguity.

# Set system timezone to UTC
sudo timedatectl set-timezone UTC

# Verify
date
# Output: Tue Mar 11 14:30:00 UTC 2026

Then convert your desired local times to UTC offsets:

You WantUTC (Winter)UTC (Summer DST)Expression
9 AM New York14:0013:000 14 * * *
9 AM London09:0008:000 9 * * *
9 AM Tokyo00:0000:000 0 * * *
9 AM Sydney22:00 (prev day)23:00 (prev day)0 22 * * *

The trade-off: when DST transitions happen, your job will shift by one hour relative to local time. A report scheduled for 9 AM EST (14:00 UTC) will arrive at 10 AM EDT when summer begins. For most backend tasks (backups, data syncs, cleanups), this is perfectly fine. For customer-facing tasks where local time matters, you need timezone-aware scheduling.

Timezone-Aware Scheduling Across Platforms

Different platforms handle timezones in different ways. Here is a practical reference:

Kubernetes CronJobs

Kubernetes added the timeZone field in v1.25 (beta) and it became stable in v1.27:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-report
spec:
  schedule: "0 9 * * *"
  timeZone: "America/New_York"  # Kubernetes >= 1.27
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: report
            image: myapp:latest
            command: ["python", "generate_report.py"]
          restartPolicy: OnFailure

Without the timeZone field, Kubernetes uses the timezone of the kube-controller-manager, which is almost always UTC.

GitHub Actions

GitHub Actions schedule triggers are always UTC. There is no timezone configuration. If you need local-time scheduling, you must calculate the UTC equivalent yourself or run a workflow every hour and check the local time in your script.

AWS CloudWatch / EventBridge

AWS schedule expressions are always UTC. For timezone-aware scheduling, use EventBridge Scheduler (not EventBridge Rules), which supports the ScheduleExpressionTimezone parameter.

Google Cloud Scheduler

GCP Cloud Scheduler requires a timezone for every job. It handles DST transitions correctly, including skipping and repeating hours. This is one of the few platforms that gets timezone handling right by default.

CronJobPro

CronJobPro lets you set a timezone on every individual job. The scheduler converts your schedule to the correct UTC execution time and adjusts automatically during DST transitions. If you schedule a job for 9 AM Eastern, it runs at 9 AM Eastern year-round, whether that is UTC-5 or UTC-4.

Step-by-Step: Diagnosing a Timezone Bug

When a cron job runs at the wrong time, work through this checklist:

  1. Confirm the server timezone.
    timedatectl | grep "Time zone"
    # Example output: Time zone: UTC (UTC, +0000)
  2. Check for CRON_TZ in the crontab.
    crontab -l | head -5
    # Look for CRON_TZ= at the top
  3. Calculate the expected UTC time. Use the cron generator to preview next execution times with timezone context.
  4. Check if DST changed recently. If the job shifted by exactly one hour, DST is almost certainly the cause.
  5. Check the cron logs.
    # Most Linux distributions log cron execution
    grep CRON /var/log/syslog | tail -20
    # or on RHEL/CentOS:
    grep CRON /var/log/cron | tail -20
  6. Verify the command's own timezone handling. If your script uses datetime.now() instead of datetime.now(timezone.utc), it inherits the process timezone, which may differ from what cron used to schedule it.

Best Practices

Store all timestamps in UTC

In your database, logs, and APIs, always use UTC. Convert to local time only at the presentation layer (UI, email templates). This eliminates an entire class of timezone bugs.

Avoid scheduling during 1:00-3:00 AM local time

If you must use local timezone scheduling, avoid the DST transition window. Schedule at 4:00 AM or later for nightly tasks. No US or European DST transition affects hours outside the 1-3 AM range.

Use IANA timezone names, never abbreviations

America/New_York is unambiguous. EST could mean Eastern Standard Time (UTC-5) or Eastern Standard Time in Australia (UTC+10).

Document the timezone for every cron job

Add a comment in your crontab: what timezone this schedule assumes, what local time it corresponds to, and who to contact if it needs to change. Future you will be grateful.

Make jobs idempotent

If a job runs twice due to a DST fall-back, it should not cause data corruption or duplicate charges. Design every scheduled task so that running it twice produces the same result as running it once.

Key Takeaways

  • Cron expressions have no timezone field. Traditional cron uses the server's system timezone, which is often UTC on cloud servers.
  • DST transitions can cause jobs to skip, run twice, or shift by one hour depending on the cron implementation.
  • Running everything in UTC eliminates DST issues entirely, but the job will shift relative to local time during transitions.
  • CRON_TZ works on most Linux systems but is not universal.
  • For guaranteed timezone-correct scheduling, use a service that supports per-job timezones and handles DST transitions automatically.

Related Articles

Never debug a timezone issue again

CronJobPro handles timezone conversions and DST transitions automatically. Set a timezone per job and your schedules stay correct year-round.