Cron and time zones: CRON_TZ, TZ and daylight saving
A crontab line has no time zone. 0 9 * * 1-5 means 9 AM in whatever time zone the cron daemon runs in, and that is often UTC on a cloud server, not the time zone of the people who care about the job. This guide shows how to find out which time zone cron uses, the ways to change it, and what happens around daylight saving time.
Which time zone does cron use?
Cron uses the system time zone it saw when the daemon started. Check it:
timedatectl | grep "Time zone" date
If you change the system time zone with timedatectl set-timezone America/New_York, restart cron afterwards (systemctl restart cron, or crond on RHEL), or it may keep using the old one. In Docker containers there is often no time zone set at all, so cron runs in UTC unless you set TZ and install the tzdata package.
Option 1: keep the server in UTC and convert
Many teams keep every server in UTC and write schedules in UTC on purpose: logs from all machines line up, and nothing shifts twice a year. The cost is doing the arithmetic yourself. 9 AM in New York is 14:00 UTC in winter and 13:00 UTC in summer, so a fixed UTC line drifts by an hour against local time when the clocks change.
Option 2: CRON_TZ in the crontab
cronie, the cron on RHEL, Rocky, AlmaLinux, Fedora and Arch, supports a time zone for the schedule:
CRON_TZ=America/New_York 0 9 * * 1-5 /srv/report.sh
The lines after CRON_TZ are read in that time zone. The cron on Debian and Ubuntu doesn’t support it: the variable is passed to the job like any other, and the schedule stays in the system time zone. Check man 5 crontab on your server before you rely on it.
TZ=... in a crontab is a different thing. It sets the time zone the job sees, so date inside the script prints local time, but the schedule itself doesn’t move.
Option 3: a systemd timer with a time zone
systemd timers accept a time zone at the end of OnCalendar:
[Timer] OnCalendar=Mon..Fri 09:00 America/New_York Persistent=true
Check how systemd reads it with systemd-analyze calendar "Mon..Fri 09:00 America/New_York". The systemd timers guide shows the rest of the setup.
Kubernetes and other schedulers
- Kubernetes CronJob: set
spec.timeZone, for exampletimeZone: "America/New_York". Without it the schedule follows the time zone of the kube-controller-manager, usually UTC. More in the CronJob guide. - Laravel:
->timezone('America/New_York')on a task, orschedule_timezoneinconfig/app.php. - Celery beat: the
timezonesetting, orcrontab(..., nowfun=...)for one task. - node-cron: the
timezoneoption ofcron.schedule.
Daylight saving time
Twice a year, local time either skips an hour or repeats one. In the United States and most of Europe, clocks move forward an hour in spring and back an hour in autumn, at night. A job scheduled inside that hour is the one that suffers:
- In spring, 2:30 AM doesn’t exist. Depending on the scheduler, the job runs right after the jump or not at all that day.
- In autumn, one hour happens twice: 1:00–2:00 in the United States, 2:00–3:00 in central Europe. A job at 1:30 or 2:30 may run twice.
Debian’s cron handles changes of less than three hours specially: jobs at fixed times that fall in the skipped period run right after the change, and jobs in the repeated hour run once. Other implementations differ. The safest choices are:
- Keep fixed-time jobs out of 1:00–3:00 local time in zones that use daylight saving.
- Make jobs safe to run twice, for example by checking whether today’s report already exists.
- Or schedule in UTC, which has no daylight saving at all.
Monitor in the same time zone
A missed run is easiest to catch when the monitor knows the schedule exactly, in the same time zone as the job. In Heartcron each check has its own cron expression and time zone, and daylight saving is handled, so 0 9 * * 1-5 in America/New_York is expected at 9 AM New York time all year:
heartcron checks create weekday-report --cron "0 9 * * 1-5" --tz America/New_York --grace 15m
Then add the ping to the job. The cron expression explainer shows the next runs in any time zone, which is a quick way to check what an expression means before and after a time change.