Back to Blog
Tutorial

Timezones and Cron Jobs: The Silent Killer of Your Schedules

Your cron job runs at the wrong time? It's probably timezones. Learn why cron uses server time (often UTC), how to fix it with CRON_TZ, and avoid the most confusing cron debugging nightmare.

9 min read
By Cron Generator Team

It's 9:00 AM. Your morning report should have generated. You check the folder. Nothing. You check the cron logs. The job ran at 5:00 AM. Four hours early.

Or maybe it's the opposite. You schedule a backup for 2:00 AM during low traffic. You wake up to complaints that the site was slow at 7:00 PM—peak traffic time. The backup ran during the day instead of at night.

What happened?

Timezones. The silent killer of cron schedules.

This is one of the most frustrating and confusing problems in cron. You set a schedule for "9 AM," but whose 9 AM? Your local time? The server's time? UTC?

This guide explains exactly what's happening and how to fix it permanently. No more mysterious schedule shifts. No more debugging timezone issues at 2 AM.

The Myth: "I Set It for 9 AM, Why Did It Run at 5 AM?"

You're on the East Coast (New York). You create a cron job to run at 9:00 AM:

0 9 * * * /usr/local/bin/generate-report.sh

You expect this to run at 9:00 AM your time (EST/EDT).

What actually happens: The job runs at 5:00 AM your time (or 2:00 PM your time, depending on the server location).

Or worse: It runs at the right time for months, then suddenly shifts by an hour when daylight saving time changes.

The Confusion

The problem stems from three different "9 AMs":

  1. Your local 9 AM - What you meant when you wrote "9"
  2. The server's 9 AM - What time it is where the physical server is located
  3. UTC 9 AM - Universal Coordinated Time (often what the server actually uses)

When you write 0 9 * * *, you're not specifying which 9 AM. Cron picks one for you—and it's probably not the one you want.

Real Examples of This Going Wrong

Scenario 1: The 5 AM Report

You: In New York (EST, UTC-5)
Server: In AWS us-east-1 (configured to UTC)
Cron job: 0 9 * * *

Expected: Report runs at 9:00 AM New York time
Reality: Report runs at 9:00 AM UTC = 4:00 AM EST (5:00 AM EDT)

Scenario 2: The Evening Backup

You: In California (PST, UTC-8)
Server: In Europe (CET, UTC+1)
Cron job: 0 2 * * *

Expected: Backup runs at 2:00 AM California time (low traffic)
Reality: Backup runs at 2:00 AM CET = 5:00 PM PST (peak traffic)

Scenario 3: The Daylight Saving Time Shift

You: In New York
Server: Set to America/New_York
Cron job: 0 9 * * *

Winter: Runs at 9:00 AM EST ✓
Spring (DST starts): Still runs at 9:00 AM EDT ✓
BUT: If server is UTC, it never shifts - now runs at 5:00 AM EDT ✗

The Truth: Cron Runs on Server System Time

Cron doesn't care about your timezone. Cron only cares about the server's system time.

Check Your Server's Timezone

Before fixing anything, know what timezone your server is using:

date

Example output:

Thu Jan  9 14:23:45 UTC 2025

The UTC at the end tells you the server is using UTC (Coordinated Universal Time).

Or check more explicitly:

timedatectl

Example output:

               Local time: Thu 2025-01-09 14:23:45 UTC
           Universal time: Thu 2025-01-09 14:23:45 UTC
                 RTC time: Thu 2025-01-09 14:23:45
                Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

Key line: Time zone: Etc/UTC (UTC, +0000)

Or just get the timezone:

cat /etc/timezone

Example output:

Etc/UTC

Common Server Timezone Configurations

| Environment | Typical Timezone | Offset from UTC | |-------------|------------------|-----------------| | AWS EC2 (default) | UTC | +0000 | | DigitalOcean (default) | UTC | +0000 | | Google Cloud (default) | UTC | +0000 | | Azure (default) | UTC | +0000 | | Heroku | UTC | +0000 | | Most Docker containers | UTC | +0000 | | Ubuntu (default install) | UTC | +0000 | | macOS (local) | Local timezone | Varies |

Notice a pattern? Almost all cloud servers default to UTC.

Why UTC Is the Default

UTC is timezone-neutral. It never observes daylight saving time. It's the same everywhere.

Benefits for servers:

  • ✅ No DST complications
  • ✅ Logs from multiple servers align perfectly
  • ✅ Timestamps are unambiguous
  • ✅ Works across global infrastructure

Problems for cron schedules:

  • ❌ Not intuitive for humans ("What time is 14:00 UTC in New York?")
  • ❌ Requires mental timezone math
  • ❌ Easy to make mistakes
  • ❌ Confusing when server and user are in different timezones

Understanding Timezone Offsets

When you see a timezone, here's what the offset means:

| Timezone | Offset | Example Cities | Math to UTC | |----------|--------|----------------|-------------| | UTC | +0000 | London (winter), Reykjavik | No conversion needed | | EST | -0500 | New York (winter) | UTC - 5 hours | | EDT | -0400 | New York (summer) | UTC - 4 hours | | PST | -0800 | Los Angeles (winter) | UTC - 8 hours | | PDT | -0700 | Los Angeles (summer) | UTC - 7 hours | | CET | +0100 | Paris (winter) | UTC + 1 hour | | CEST | +0200 | Paris (summer) | UTC + 2 hours | | IST | +0530 | Mumbai (no DST) | UTC + 5:30 hours | | JST | +0900 | Tokyo (no DST) | UTC + 9 hours | | AEST | +1000 | Sydney (winter) | UTC + 10 hours |

Converting Timezone to UTC

You want: 9:00 AM New York time (EST, UTC-5)

9:00 AM EST = 9:00 + 5 = 14:00 UTC (2:00 PM UTC)

Cron job:

0 14 * * *  # Runs at 2 PM UTC = 9 AM EST

But wait! When daylight saving time starts:

9:00 AM EDT = 9:00 + 4 = 13:00 UTC (1:00 PM UTC)

Now your cron job is wrong. It runs at 2 PM UTC = 10 AM EDT (one hour late).

This is why manual UTC conversion is painful.

The Fix: CRON_TZ Variable

Modern cron implementations support the CRON_TZ variable, which lets you specify a timezone for your cron jobs.

Basic Syntax

# In crontab -e
CRON_TZ=America/New_York

0 9 * * * /usr/local/bin/generate-report.sh

What this does:

  • The cron job runs at 9:00 AM in the America/New_York timezone
  • Automatically handles daylight saving time
  • No manual UTC conversion needed
  • Works regardless of server timezone

Supported Timezones

Use IANA timezone database names (also called "tz database" or "Olson database").

Format: Continent/City

Common timezones:

| Timezone Name | Description | |---------------|-------------| | America/New_York | Eastern Time (EST/EDT) | | America/Chicago | Central Time (CST/CDT) | | America/Denver | Mountain Time (MST/MDT) | | America/Los_Angeles | Pacific Time (PST/PDT) | | America/Phoenix | Arizona (no DST) | | America/Toronto | Eastern Time (Canada) | | America/Sao_Paulo | Brazil | | Europe/London | GMT/BST | | Europe/Paris | CET/CEST | | Europe/Berlin | CET/CEST | | Asia/Tokyo | Japan (no DST) | | Asia/Shanghai | China (no DST) | | Asia/Kolkata | India (no DST) | | Australia/Sydney | Australian Eastern Time | | UTC | Coordinated Universal Time |

Find your timezone:

timedatectl list-timezones

Or see the full list: IANA Timezone Database

Important: Don't Use Abbreviations

Wrong (don't use):

CRON_TZ=EST  # Don't use abbreviations
CRON_TZ=PST
CRON_TZ=GMT

Correct (use full names):

CRON_TZ=America/New_York
CRON_TZ=America/Los_Angeles
CRON_TZ=Europe/London

Why? Abbreviations are ambiguous:

  • EST could be US Eastern or Australian Eastern
  • CST could be US Central, China, or Cuba
  • Full names are precise and unambiguous

Check if Your Cron Supports CRON_TZ

Not all cron implementations support CRON_TZ. Check which cron you're running:

crontab -V

Cron implementations that support CRON_TZ:

  • ✅ Vixie Cron (most Linux distributions)
  • ✅ ISC Cron
  • ✅ cronie (Red Hat, CentOS, Fedora)
  • ✅ bcron
  • ✅ systemd timers (different syntax)

If not supported:

  • ❌ Very old cron versions
  • ❌ Some BSD variants (use different syntax)

Test it:

# Add a test job with CRON_TZ
crontab -e
CRON_TZ=America/New_York
* * * * * date >> /tmp/cron-tz-test.txt

Wait a few minutes, then check:

cat /tmp/cron-tz-test.txt

If you see timestamps in New York time, it works!

If you see UTC timestamps, CRON_TZ isn't supported (see workarounds below).

Real-World Example: New York Business Hours

You want to run a report every weekday at 9:00 AM Eastern Time.

The Wrong Way (Without CRON_TZ)

# Server is on UTC
# You calculate: 9 AM EST = 2 PM UTC
0 14 * * 1-5 /usr/local/bin/generate-report.sh

Problems:

  1. Winter (EST, UTC-5):

    • 2 PM UTC = 9 AM EST ✓ Correct
  2. Summer (EDT, UTC-4):

    • 2 PM UTC = 10 AM EDT ✗ One hour late
  3. DST transitions:

    • Spring forward: Report runs late for 6 months
    • Fall back: Report runs early for 6 months

You have to manually update the cron job twice a year or accept the wrong time half the year.

The Right Way (With CRON_TZ)

# In crontab -e
CRON_TZ=America/New_York

# Run every weekday at 9 AM Eastern Time
0 9 * * 1-5 /usr/local/bin/generate-report.sh

Result:

  • ✅ Winter: Runs at 9:00 AM EST
  • ✅ Summer: Runs at 9:00 AM EDT
  • ✅ DST transitions: Automatically adjusts
  • ✅ No manual updates needed

Set it once, forget it forever.

Complete Example: Multiple Timezones

You have users in different timezones and need to run jobs at local times for each region.

Multi-Timezone Crontab

# File: crontab -e

# Default timezone for most jobs
CRON_TZ=America/New_York

# Eastern time jobs
0 9 * * 1-5 /usr/local/bin/east-coast-report.sh
0 17 * * 1-5 /usr/local/bin/east-coast-eod-summary.sh

# Pacific time jobs (override with new CRON_TZ)
CRON_TZ=America/Los_Angeles
0 9 * * 1-5 /usr/local/bin/west-coast-report.sh
0 17 * * 1-5 /usr/local/bin/west-coast-eod-summary.sh

# European jobs
CRON_TZ=Europe/London
0 9 * * 1-5 /usr/local/bin/uk-report.sh

CRON_TZ=Europe/Paris
0 9 * * 1-5 /usr/local/bin/eu-report.sh

# UTC jobs (monitoring, backups)
CRON_TZ=UTC
0 */6 * * * /usr/local/bin/health-check.sh
0 3 * * * /usr/local/bin/backup-database.sh

How this works:

  • Each CRON_TZ line sets the timezone for all jobs below it until the next CRON_TZ
  • You can change timezone multiple times in one crontab
  • Jobs run at the correct local time for their region
  • No manual UTC conversion needed

Per-Job Timezone (Alternative Syntax)

Some cron implementations allow per-job timezone specification:

# Timezone specified inline (not all crons support this)
CRON_TZ=America/New_York 0 9 * * * /usr/local/bin/report.sh

Or using environment variable in the command:

0 9 * * * TZ=America/New_York /usr/local/bin/report.sh

Test this on your system as support varies.

Workarounds When CRON_TZ Isn't Supported

If your cron doesn't support CRON_TZ, here are alternatives:

Option 1: Change Server Timezone

Set the entire server to your timezone:

# List available timezones
timedatectl list-timezones

# Set server timezone
sudo timedatectl set-timezone America/New_York

# Verify
timedatectl

Now all cron jobs run in that timezone.

Pros:

  • ✅ Simple to understand
  • ✅ Works with any cron version
  • ✅ Automatic DST handling

Cons:

  • ❌ Affects entire system (may break other things)
  • ❌ Not practical for multi-timezone applications
  • ❌ Server logs now in local time (harder to correlate across servers)

Option 2: Use TZ Environment Variable in Script

Set timezone inside your script:

#!/bin/bash
# File: /usr/local/bin/generate-report.sh

# Set timezone for this script
export TZ=America/New_York

# Now all date commands use New York time
echo "Report generated at $(date)" >> /var/log/report.log

# Your script logic here

Cron job (runs at UTC time, but script uses NY time):

# This still runs at UTC 2 PM
0 14 * * * /usr/local/bin/generate-report.sh

Pros:

  • ✅ Works with any cron version
  • ✅ Different scripts can use different timezones

Cons:

  • ❌ You still have to convert schedule to UTC manually
  • ❌ Schedule breaks during DST transitions
  • ❌ Script knows its timezone, but cron schedule doesn't match

Option 3: Use systemd Timers (Modern Alternative)

If you're on a systemd-based Linux distribution, use timers instead of cron:

Create timer file:

# File: /etc/systemd/system/report.timer
[Unit]
Description=Run daily report at 9 AM New York time

[Timer]
OnCalendar=09:00:00
Timezone=America/New_York
Persistent=true

[Install]
WantedBy=timers.target

Create service file:

# File: /etc/systemd/system/report.service
[Unit]
Description=Generate daily report

[Service]
Type=oneshot
ExecStart=/usr/local/bin/generate-report.sh

Enable and start:

sudo systemctl enable report.timer
sudo systemctl start report.timer

# Check status
systemctl list-timers

Pros:

  • ✅ Native timezone support
  • ✅ More powerful than cron
  • ✅ Better logging
  • ✅ Dependency management

Cons:

  • ❌ Different syntax to learn
  • ❌ Not available on all systems
  • ❌ More complex setup

Daylight Saving Time: The Double-Edged Sword

When you use CRON_TZ with a timezone that observes DST, be aware of two special moments each year:

Spring Forward (DST Starts)

Example: America/New_York in Spring

At 2:00 AM, clocks jump to 3:00 AM. The 2:00 AM hour doesn't exist.

CRON_TZ=America/New_York
0 2 * * * /usr/local/bin/backup.sh

What happens on DST transition day?

Most cron implementations skip the job since 2:00 AM never occurs.

Solution: Schedule critical jobs outside the 2:00-3:00 AM window:

# Safe: Runs at 1:00 AM or 4:00 AM
0 1 * * * /usr/local/bin/backup.sh
0 4 * * * /usr/local/bin/backup.sh

Or use a timezone that doesn't observe DST:

CRON_TZ=America/Phoenix  # Arizona - no DST
0 2 * * * /usr/local/bin/backup.sh

Fall Back (DST Ends)

Example: America/New_York in Fall

At 2:00 AM, clocks jump back to 1:00 AM. The 1:00-2:00 AM hour happens twice.

CRON_TZ=America/New_York
0 1 * * * /usr/local/bin/backup.sh

What happens on DST transition day?

Most cron implementations run the job twice—once at "first 1:00 AM" and again at "second 1:00 AM."

If your job isn't idempotent, this could cause problems:

  • Database backup runs twice (wastes space)
  • Email report sent twice (users get duplicates)
  • Payment processing runs twice (charges customers twice!)

Solution 1: Make your script idempotent:

#!/bin/bash
LOCK_FILE=/var/run/backup.lock

# Check if already running
if [ -f "$LOCK_FILE" ]; then
    echo "Backup already running, exiting"
    exit 0
fi

# Create lock
touch "$LOCK_FILE"

# Do backup
# ...

# Remove lock
rm "$LOCK_FILE"

Solution 2: Avoid the 1:00-2:00 AM window on DST transition days.

Solution 3: Use UTC for critical jobs:

CRON_TZ=UTC
0 6 * * * /usr/local/bin/backup.sh  # Always runs exactly once

Testing Your Timezone Configuration

Before trusting your timezone setup in production, test it.

Test 1: Verify Timezone Recognition

# Add a test job that prints date
crontab -e
CRON_TZ=America/New_York
* * * * * date >> /tmp/tz-test.txt

Wait 2 minutes, then check:

cat /tmp/tz-test.txt

Expected output:

Thu Jan  9 09:15:01 EST 2025
Thu Jan  9 09:16:01 EST 2025

If you see UTC or wrong timezone, CRON_TZ isn't working.

Test 2: Compare Multiple Timezones

CRON_TZ=UTC
* * * * * echo "UTC: $(date)" >> /tmp/multi-tz-test.txt

CRON_TZ=America/New_York
* * * * * echo "NY: $(date)" >> /tmp/multi-tz-test.txt

CRON_TZ=America/Los_Angeles
* * * * * echo "LA: $(date)" >> /tmp/multi-tz-test.txt

Expected output:

UTC: Thu Jan  9 14:20:01 UTC 2025
NY: Thu Jan  9 09:20:01 EST 2025
LA: Thu Jan  9 06:20:01 PST 2025

All three timestamps represent the same moment, just displayed in different timezones.

Test 3: Verify DST Handling

Test near a DST transition (spring or fall). Check that jobs run at the correct local time before and after the transition.

Or force a date change (if you have a test server):

# Set date to day before DST
sudo date -s "2025-03-08 23:00:00"

# Verify your cron runs correctly at transition

Common Timezone Mistakes

Mistake 1: Using Abbreviations

Wrong:

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

Correct:

CRON_TZ=America/Los_Angeles
0 9 * * * /usr/local/bin/report.sh

Mistake 2: Assuming Server Timezone

Wrong assumption:

# "The server is in Virginia, so it must be Eastern Time"
0 9 * * * /usr/local/bin/report.sh

Reality: Most cloud servers are UTC regardless of physical location.

Correct:

# Explicitly set timezone
CRON_TZ=America/New_York
0 9 * * * /usr/local/bin/report.sh

Mistake 3: Not Testing DST Transitions

Wrong:

# Looks good in January...
CRON_TZ=America/New_York
0 2 * * * /usr/local/bin/critical-backup.sh

Problem: Skipped on DST spring-forward day.

Correct:

# Avoid 2 AM for critical jobs
CRON_TZ=America/New_York
0 1 * * * /usr/local/bin/critical-backup.sh

Mistake 4: Mixing Timezone Strategies

Wrong:

# Some jobs use CRON_TZ, others use manual UTC conversion
CRON_TZ=America/New_York
0 9 * * * /usr/local/bin/report.sh

# This is UTC 2 PM... wait, that's 9 AM EST in winter but 10 AM EDT in summer!
0 14 * * * /usr/local/bin/backup.sh

Correct (be consistent):

CRON_TZ=America/New_York
0 9 * * * /usr/local/bin/report.sh
0 14 * * * /usr/local/bin/backup.sh  # 2 PM Eastern Time year-round

Or:

# All jobs explicitly set to UTC
CRON_TZ=UTC
0 14 * * * /usr/local/bin/report.sh     # 9 AM EST / 10 AM EDT
0 19 * * * /usr/local/bin/backup.sh     # 2 PM EST / 3 PM EDT

Pick one strategy and stick to it.

Quick Reference: Timezone Commands

| Task | Command | |------|---------| | Check server timezone | date or timedatectl | | List all timezones | timedatectl list-timezones | | Change server timezone | sudo timedatectl set-timezone America/New_York | | Test CRON_TZ support | Add test job and verify output | | Find your timezone name | IANA Timezone List |

When to Use Each Approach

| Scenario | Best Solution | |----------|---------------| | Single timezone, all jobs same local time | Set CRON_TZ once at top of crontab | | Multiple timezones, different jobs | Multiple CRON_TZ declarations | | Critical 24/7 jobs, no DST complications | CRON_TZ=UTC | | Server supports systemd | Use systemd timers with Timezone= | | Old cron, no CRON_TZ support | Change server timezone or use workaround scripts | | Job must run at exact UTC time | CRON_TZ=UTC or don't set CRON_TZ at all |

Conclusion: Timezones Don't Have to Be Silent Killers

The timezone problem:

  • Cron uses server time (usually UTC)
  • You think in local time (EST, PST, etc.)
  • Schedules break during DST transitions
  • Debugging is painful and time-consuming

The solution:

  • Use CRON_TZ to explicitly set timezone
  • Use full timezone names (America/New_York, not EST)
  • Test your configuration before relying on it
  • Be aware of DST transition edge cases
  • Make critical jobs idempotent

Your checklist:

  1. ✅ Check your server's current timezone: date
  2. ✅ Determine which timezone your jobs should use
  3. ✅ Add CRON_TZ=Your/Timezone to crontab
  4. ✅ Test with a frequent job to verify it works
  5. ✅ Avoid scheduling critical jobs at 1-2 AM (DST risk)
  6. ✅ Document why you chose that timezone

Before (painful):

# Is this UTC? Server time? My time? Who knows!
0 9 * * * /usr/local/bin/report.sh

After (clear):

# Explicitly runs at 9 AM New York time, handles DST automatically
CRON_TZ=America/New_York
0 9 * * * /usr/local/bin/report.sh

Stop debugging timezone issues at 2 AM. Set CRON_TZ and sleep soundly knowing your jobs run when they should.

Ready to create timezone-aware cron schedules? Use our Cron Expression Generator to build and test your schedules with confidence.


Related Articles

Essential Troubleshooting:

Master the Basics:

Production-Ready Examples:


Keywords: cron timezone, crontab timezone, cron job wrong time, cron_tz, cron utc, cron daylight saving time, cron dst, timezone cron jobs, cron server time, cron local time