I scheduled a heavy maintenance job to run Saturday at 6am, while I was asleep and nothing else was using the machine. Weeks later I checked the logs. It had been firing every Saturday at 8pm, right in the middle of my most active hours.
The job was fine. The schedule was fine. My assumption about what the schedule meant was wrong.
The comment lied, and cron didn't care
Here is what the crontab line looked like:
# Saturday 06:00 UTC -- weekend maintenance, runs while I'm asleep
0 6 * * 6 /home/me/bin/weekend-maintenance.sh
The comment says UTC. I wrote it that way because I think in UTC for infrastructure. But that comment is just text. Cron never reads it.
My machine's local timezone is AEST (UTC+10). I had not set CRON_TZ anywhere in the crontab. So cron read 0 6 as 6am local AEST, not 6am UTC. The two are 10 hours apart.
Every time-of-day job in that crontab was firing 10 hours away from what its comment claimed. Eight of them. A morning report meant to land before I woke up fired in the evening. A "run this when the machine is quiet" job ran at peak. None of them errored, so nothing flagged it.
Why cron does this
Cron schedules in the system's local timezone by default. On Linux that is whatever the system clock is set to, or the TZ the daemon inherited. It is not UTC unless your machine is set to UTC.
You can confirm your machine's timezone in one command:
timedatectl
# Local time: Sat 2026-06-20 20:00:13 AEST
# Universal time: Sat 2026-06-20 10:00:13 UTC
# Time zone: Australia/Brisbane (AEST, +1000)
There is the 10 hour gap, right in the output. Local 20:00 is 10:00 UTC.
How to detect it on your own boxes
The fastest check is to compare what a job's comment claims against when it actually ran. Cron logs each invocation. On most systems:
# Debian / Ubuntu
grep CRON /var/log/syslog | grep weekend-maintenance
# RHEL / Fedora, or anything on systemd
journalctl -t CRON --since "14 days ago" | grep weekend-maintenance
Here is the line that gave me away. The comment promised 06:00 UTC. The log disagreed:
# crontab comment claims 06:00 UTC. Reality:
Jun 20 20:00:01 box CRON[12491]: (me) CMD (/home/me/bin/weekend-maintenance.sh)
20:00 local, not 06:00. Line up those timestamps with the hour you THINK the job runs. If they disagree by exactly your UTC offset, you have found it.
The fix: stop writing comments you aren't enforcing
There are two honest ways out, and which one you pick depends on the job.
For jobs whose intent is local ("run while I'm asleep", "send before the workday"), write the schedule in local time and label it as local. No surprise, no conversion in your head:
# Saturday 06:00 AEST (machine-local) -- weekend maintenance
0 6 * * 6 /home/me/bin/weekend-maintenance.sh
For jobs whose intent is a real fixed instant (a market-hours alert, a cross-region batch that must line up with UTC), set CRON_TZ explicitly at the top of the block:
CRON_TZ=UTC
# Now 0 6 genuinely means 06:00 UTC, whatever the machine's clock says
0 6 * * 6 /home/me/bin/utc-batch.sh
CRON_TZ overrides the timezone for every entry below it, until the next CRON_TZ. The standard cron daemons on mainstream Linux (Vixie and cronie) support it. It is not POSIX, so check your daemon if you run something exotic.
The gotcha that bit me twice: CRON_TZ is not TZ
One job genuinely needed US market hours, so I reached for CRON_TZ=America/New_York. That is correct, and naming a region handles US daylight saving automatically, which is the whole reason to use a region name instead of a fixed offset.
But CRON_TZ only controls WHEN the job fires. It does not set the timezone inside your script. The process still inherits the system TZ. If your script formats a timestamp or does date math, it uses the machine's timezone, not the one you scheduled in. If you need both, set both:
CRON_TZ=America/New_York
0 9 * * 1-5 TZ=America/New_York /home/me/bin/market-open.sh
That inline TZ= works because cron runs each job line through a shell, so a leading VAR=value becomes an environment assignment for the command.
Daylight saving still wants to ruin your day
Naming a region (America/New_York) instead of a fixed offset means cron tracks DST for you. Good. But DST itself has two sharp edges, and CRON_TZ does not file them down:
- On spring-forward, the 02:00 to 03:00 hour does not exist. A job scheduled at 02:30 just does not run that day.
- On fall-back, that hour happens twice. A job inside it can run twice.
The boring fix is to keep anything important out of the 01:00 to 03:00 window. Schedule at 04:00 or later and DST never touches you.
Back to the Saturday job
The fix for the job that started all this was the boring one. I rewrote the comment to match the mechanism and left the schedule alone:
# Saturday 06:00 AEST (machine-local) -- weekend maintenance
0 6 * * 6 /home/me/bin/weekend-maintenance.sh
The next Saturday, journalctl -t CRON showed it firing at 06:00 AEST. Machine quiet, me asleep, finally doing what the comment always claimed.
The actual lesson
The bug was not cron. Cron did exactly what it always does. The bug was that I trusted a comment to enforce behavior it had no power over.
A # runs at 09:00 UTC comment is documentation. It is the weakest thing in the system, because nothing checks it against reality. The schedule is the enforcement. When the two drift apart, the schedule wins, and you do not get an error. You get ten hours of silent wrongness.
The rule I follow now is small. Write each schedule in the timezone you actually mean. Set CRON_TZ (and TZ) when correctness depends on it. And never let "the server is probably UTC" be the load-bearing assumption.
Go run timedatectl on your boxes, then read your crontab comments back as claims instead of facts. You might have a job that has been quietly firing at the wrong hour for months. I did.













