viewer.csslab.dev

Cron, containers, and the time zone nobody set

Because a cron expression names a wall-clock time, not an instant, and the wall clock belongs to whatever machine runs the scheduler. Containers almost always default to UTC while the person writing the schedule is thinking in local time, so a job set for 09:00 fires at 18:00 in Seoul. The expression is not wrong; it is being read against a different clock than the one you had in mind.

The expression has no time zone in it

Five fields describe minute, hour, day, month, and weekday. There is nowhere in that syntax to say which zone those numbers are in, so the scheduler uses its own — the system zone on a Unix host, the JVM default for Spring's @Scheduled, or whatever the orchestrator injected into the container.

A base image without tzdata configured runs in UTC. That is the single most common cause of a schedule being off by exactly the local offset: nine hours in Korea, eight or nine in Central Europe depending on the season.

The fix is to be explicit rather than to compensate. Set the zone where the scheduler supports it — Kubernetes CronJob has timeZone, Quartz has TimeZone on the trigger, Spring has zone on the annotation — or set the container's TZ and make it part of the deployment rather than something inherited.

Daylight saving makes some hours vanish and others repeat

Where DST applies, local time is not continuous. On the spring transition the clock jumps from 01:59 straight to 03:00, so a job scheduled at 02:00 has no moment to run in and simply does not fire that day. On the autumn transition 02:00 happens twice, and depending on the implementation the job runs once or twice.

Implementations disagree about what to do. Vixie cron nudges jobs that fell in the gap so they still run; many container schedulers do not. Relying on the specific behaviour of one scheduler is how a schedule breaks when the platform changes underneath it.

Korea does not observe DST, so a KST-configured scheduler is not exposed to this. It matters as soon as the same expression runs in a US or European region, which is exactly what happens when a service is deployed to a second cluster.

What to do

Run schedulers in UTC and write expressions in UTC. It removes both problems at once: no gaps, no repeats, no per-region drift. The cost is that a human reading 0 0 * * * has to do the conversion, which is a smaller cost than a job that silently skips a day twice a year.

If a job genuinely must follow local time — a report that has to land before the workday starts — set the zone explicitly and avoid scheduling anything between 01:00 and 03:00 local, which is the window where the transitions happen.

For anything important, log the instant the job actually ran alongside the schedule it was supposed to follow. A job that fires at the wrong time is invisible until someone compares the two.

Tools for this

Specifications this follows

Updated 2026-09-05