Full lesson
Explore the full explanation, examples, and visuals at your own pace.
daily-report: 0 9 * * *
Your CronJob says nine o'clock, but it fires late. The daily-report job, written for a 9 AM Berlin run, shows up at 10 in winter and 11 in summer. The chart shows that gap. Because the delay changes with the season, this isn't random drift. Something is shifting the clock.
Chart values
| Case | Hour (Berlin) |
|---|---|
| Intended | 9 |
| Winter actual | 10 |
| Summer actual | 11 |
No timeZone set: controller clock wins
Here's the cause. With no timeZone set, kube-controller-manager reads the schedule in its own clock, which is often UTC, especially on managed clusters. So 9 AM means 09:00 UTC. On the left, the Berlin clock reads 10:00 in winter. On the right, the controller clock reads 09:00. Same instant, two zones.
UTC stays fixed; Berlin's offset shifts
The controller's 09:00 UTC never moves. What changes is Berlin's offset. In winter, Central European Time is UTC plus one, so the job lands at 10:00 Berlin. In summer, daylight saving makes it UTC plus two, so the same run lands at 11:00. The local hour jumps twice a year.
daily-report.yaml (before)
Here's the manifest as the developer wrote it. Look at the schedule line, zero nine, star, star, star. It says nine o'clock, but nothing in this file says whose nine o'clock. There's no timeZone field, so the controller's default zone quietly decides, and on many clusters that's UTC.
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-report
spec:
schedule: "0 9 * * *" # no timeZone set
jobTemplate:
spec:
template:
spec:
containers:
- name: report
image: report:1.0
restartPolicy: OnFailureThe fix: spec.timeZone
The fix is one field, spec.timeZone, set to an IANA zone name like Europe/Berlin. Now 0 9 * * * means nine in Berlin, year-round. It's stable since Kubernetes v1.27. Don't put TZ or CRON_TZ inside the schedule string. Kubernetes rejects those prefixes.
spec:
schedule: "0 9 * * *"
timeZone: "Europe/Berlin" # IANA zone name
# Not supported - rejected by Kubernetes:
# schedule: "CRON_TZ=Europe/Berlin 0 9 * * *"
# schedule: "TZ=Europe/Berlin 0 9 * * *"Two different clocks
These are two separate clocks. On the left, spec.timeZone tells the CronJob controller when to start the Job. That's all it does. On the right, the container has its own zone, which shapes log timestamps and any time logic in your app. That's usually UTC, unless the image or a TZ environment variable changes it.
Local zones inherit DST edge cases
A local zone brings daylight saving edge cases with it. When clocks spring forward in Berlin, 02:30 never happens, so a job scheduled there can be skipped. When they fall back, that hour happens twice, so behavior can surprise you. Avoid scheduling inside the transition window.
Verify the schedule (illustrative output)
To verify the fix, run kubectl get cronjob and check the TIMEZONE column. It should say Europe/Berlin. Then read lastScheduleTime. Kubernetes stores it in UTC, so 08:00 with a Z is 09:00 in Berlin during winter. That confirms the schedule follows local time. This output is illustrative.
$ kubectl get cronjob daily-report
NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE
daily-report 0 9 * * * Europe/Berlin False 0 3h 5d
$ kubectl get cronjob daily-report \
-o jsonpath='{.status.lastScheduleTime}'
2024-01-15T08:00:00Z # = 09:00 Berlin (CET)Which clock should the job follow?
Choose UTC for system tasks that need even spacing, since it never shifts with DST. Choose local time for human-facing jobs like the Berlin report. The cost is edge cases, so keep local schedules out of the 02:00 to 03:00 transition window.
- UTC: fixed interval, no DST shifts
- Local: tracks business hours like 9 AM
- Local: avoid the 02:00-03:00 window
Part 10
A cron schedule is meaningless without its clock. Kubernetes uses the controller's zone unless spec.timeZone names an IANA zone. So decide deliberately between UTC and local time, set spec.timeZone yourself, and don't trust the default.
- Set spec.timeZone explicitly for local-time jobs
- Check your cluster version supports it
- Verify last schedule time with kubectl get cronjob



