Cron on Kubernetes vs Linux Crontab: Timezone Traps
Why the same five-field cron string fires at different times on GitHub Actions, K8s CronJob, and server crontab—and how to preview safely.
0 9 * * 1-5 looks unambiguous: minute zero, hour nine, weekdays. Until you deploy it on three platforms and backups run at 09:00 local, 09:00 UTC, and 09:00 in the control plane’s zone—all from the same string.
This comparison is for engineers who copy cron lines between Linux servers, Kubernetes, and CI without re-reading timezone rules.
Same grammar, different clocks
Classic Unix cron uses five fields (minute, hour, day-of-month, month, day-of-week). The grammar is familiar; the clock is not shared:
| Platform | Typical timezone behavior |
|---|---|
Linux user crontab | Server local timezone (timedatectl) |
Kubernetes CronJob | Controller default unless timeZone set (1.25+) |
GitHub Actions schedule | Always UTC |
| Quartz / some cloud rules | Often six fields (seconds first) |
A line copied from /etc/crontab on a Shanghai VM to a GitHub workflow will shift by eight hours unless you convert.
Linux crontab: know the server
crontab -e on a VM evaluates expressions in the machine’s local zone. DST changes can skip or repeat an hour for wall-clock schedules like 30 2 * * *.
Runbooks should record:
- Expression
- Server
timedatectloutput - Expected UTC equivalent for log correlation
When correlating with CloudWatch or Loki timestamps, convert with a Timestamp Converter instead of mental math during an incident.
Kubernetes: set timeZone explicitly
Since Kubernetes 1.25, CronJob supports:
spec:
timeZone: "Asia/Shanghai"
schedule: "0 9 * * 1-5"
Without timeZone, behavior depends on controller configuration—do not assume it matches your laptop. Treat missing timezone in YAML as a review blocker.
Cluster upgrades and multi-region control planes are why explicit IANA zones beat "we always use local."
GitHub Actions: UTC only
on:
schedule:
- cron: "0 9 * * 1-5"
This is 09:00 UTC Monday–Friday. For Shanghai weekday 09:00, you need 0 1 * * 1-5 in UTC during standard offset—or document the offset table in the workflow README.
Many "why did CI run at night?" tickets start here.
Preview before merge
Regardless of platform:
- Parse the expression in a Cron Expression Parser
- Read the human description (watch day-of-month OR day-of-week semantics)
- Preview next runs in UTC and the target zone
- Add timezone + platform to the runbook line
Common copy-paste failures
| Mistake | Result |
|---|---|
| Actions cron meant as local 9am | Job at wrong wall time |
K8s without timeZone | Drift after cluster move |
| Six-field Quartz pasted as five-field | Invalid or wrong schedule |
| DOM + DOW both set expecting AND | Extra unexpected runs |
Related learning
Field syntax, platform tables, and a full debugging checklist live in the Cron Expressions course. For log correlation across zones, pair with Timestamps in logs across timezones.
Bottom line
Cron strings are timezone-agnostic; schedulers are not. Document platform + zone next to every line, preview in UTC and local, and set Kubernetes timeZone on purpose—not by accident.