CronJob Schedule Builder
Build a Kubernetes CronJob YAML with timezone awareness. Preview the next runs in your local time. No data leaves your browser.
Schedule
timeZone field. Older clusters interpret schedules in the kube-controller-manager's local timezone (usually UTC).Job configuration
["/bin/sh", "-c", ...] so shell features like $VAR and $(cmd) work.apiVersion: batch/v1
kind: CronJob
metadata:
name: my-cronjob
spec:
schedule: "0 9 * * 1-5"
timeZone: "UTC"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
startingDeadlineSeconds: 200
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: my-cronjob
image: "busybox:1.36"
command: ["/bin/sh", "-c", "echo \"Hello from $(hostname)\""]
resources:
requests:
cpu: 100m
memory: 128Mi
How K8s CronJob schedules work
A Kubernetes CronJob schedule is a standard 5-field Unix cron expression: minute hour day-of-month month day-of-week. Each field accepts a wildcard * (every value), an integer (9), a range (1-5), a list (1,3,5), or a step (*/15 or 0-30/5). For day-of-week, both 0 and 7 mean Sunday, and you can use SUN–SAT as case-insensitive aliases. Months accept JAN–DEC the same way.
Kubernetes also accepts the macro shortcuts @hourly, @daily (or @midnight), @weekly, @monthly, and @yearly (or @annually). One macro Kubernetes does not accept is @reboot — it has no equivalent in CronJob's model and won't be parsed.
Without spec.timeZone, your schedule runs in whatever timezone the kube-controller-manager is set to. That's almost always UTC. "9 AM daily" in the YAML means 9 AM UTC — three hours early in Berlin, four hours late in San Francisco.
The Quartz extensions you may have seen in Spring or AWS EventBridge schedules — ?, L, W, #, and the leading seconds field — are not supported. If you paste a 6-field schedule or one containing ?, this tool flags it specifically so you can convert before applying. There's also a quiet edge case: when both day-of-month and day-of-week are restricted, Kubernetes uses Vixie-cron OR semantics — a day matches if either field matches, not both.
Timezone support (Kubernetes 1.25+)
Kubernetes 1.25 added the spec.timeZone field as a beta feature, and it went stable in 1.27. The value is any IANA time zone name — America/New_York, Europe/Berlin, Asia/Tokyo. Once set, Kubernetes interprets the schedule in that zone and handles daylight saving transitions automatically. A 9 AM schedule in America/New_York stays at 9 AM Eastern Time year-round, even though the underlying UTC instant shifts by an hour twice a year.
Quick fix. On Kubernetes 1.25+, set spec.timeZone to an IANA zone and stop hand-encoding offsets in the schedule itself. Kubernetes handles DST for you — the offsets you'd hand-write don't.
For clusters older than 1.25 — or any cluster where the feature gate is off — the workaround is to set TZ as an environment variable inside the container and write the cron expression in UTC. That works for time-of-day inside the container, but it doesn't move the actual triggering time, so DST transitions will need manual schedule updates. If you control the cluster version, prefer upgrading.
Choosing concurrencyPolicy
concurrencyPolicy tells the controller what to do when a previous Job from this CronJob is still running and the next scheduled run arrives.
- Allow (default): both runs execute concurrently. Safe for stateless work where overlap doesn't break anything — log shippers, metric scrapers, idempotent fan-outs.
- Forbid: the new run is skipped while the previous is still active. Use when the workload mutates shared state and a second concurrent invocation would cause double-billing, double-emails, or contention — backups, billing reconciliation, ETL into a single destination.
- Replace: the previous Job is killed and the new one starts. Use when only the most recent result matters and a long-running prior run is wasted work — sync jobs, cache rebuilds.
"Forbid" is the safest default for any new schedule until you know the workload is comfortable overlapping. Defaulting to "Allow" is the cause of plenty of "why did my job run twice?" incident reviews.
Handling missed runs
Two settings control what happens when the controller can't fire a run on time — usually because of control-plane unavailability, controller backpressure, or pod scheduling delays.
startingDeadlineSeconds is the window during which a missed run can still start. Set it longer than your worst-case scheduling delay; 200 seconds is a sane default for most workloads, and stricter SLA work might use a shorter window. Without it, the controller has no upper bound on lateness — and that's where the next setting becomes load-bearing.
The 100-missed-runs landmine. Kubernetes tracks missed schedules per CronJob. Once 100 missed schedules accumulate without a successful run, the controller logs an error and stops creating new Jobs entirely. A long control-plane incident, a paused namespace, a brief etcd hiccup — and suddenly your nightly job has been silent for a week. Setting startingDeadlineSeconds defends against accumulation, because runs missed by more than the deadline aren't counted toward the 100 limit.
Don't set timeZone via TZ env var if you're on 1.25+. The TZ-inside-the-container trick changes the time inside the running pod but not when the controller fires the schedule. On modern clusters, spec.timeZone moves the actual trigger time and handles DST. Mixing both gets you a pod that thinks it's 9 AM local while the controller thinks it's 9 AM UTC — debugging that combination is exactly as fun as it sounds.
FAQ
Why is my CronJob running at the wrong time?
Almost always a timezone problem. Without an explicit timeZone field, Kubernetes interprets your schedule in the kube-controller-manager's local timezone — which on most clusters is UTC. A schedule like 0 9 * * * fires at 9 AM UTC, not 9 AM in your office. On Kubernetes 1.25+, set spec.timeZone (e.g. America/New_York) so the schedule means what you wrote.
What's the difference between Allow, Forbid, and Replace?
Allow lets a new run start even if the previous one is still going (use for stateless workers). Forbid skips the new run if the previous is still active (use when only one instance should mutate state). Replace kills the previous Job and starts the new one (use when only the latest result matters).
Can I use seconds in a Kubernetes CronJob schedule?
No. Kubernetes uses standard 5-field Unix cron (minute, hour, day-of-month, month, day-of-week). The 6-field form with seconds is Quartz syntax, which Kubernetes does not implement. For sub-minute scheduling, run a long-lived workload that sleeps internally instead — CronJob is the wrong tool below the minute boundary.
What happens if a CronJob misses its scheduled run?
If startingDeadlineSeconds is set, the controller can still launch the run within that window. Without it, the controller tracks missed schedules and stops creating new runs once 100 are missed — a common production landmine after long control-plane outages. Set startingDeadlineSeconds to something larger than your worst-case scheduling delay (200s is a sane starting point).
How do I migrate a CronJob from UTC to a specific timezone?
On Kubernetes 1.25+, add spec.timeZone with an IANA name like Europe/Berlin and remove any UTC-offset hacks from the schedule itself. Kubernetes handles DST automatically once the field is set. On older clusters that don't support timeZone, the workaround is to set the TZ environment variable inside the container and adjust the cron expression to UTC offsets, accepting that DST transitions will need manual updates.
Why is my cron expression with ? not working?
The ? character is Quartz syntax for "no specific value," used because Quartz disallows specifying both day-of-month and day-of-week. Kubernetes uses Unix cron, which has no ?: just write the field as *. So 0 9 ? * MON-FRI in Quartz becomes 0 9 * * 1-5 in Kubernetes.