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

cron + timezone + presets
At 09:00 AM, Monday through Friday
K8s 1.25+ supports the timeZone field. Older clusters interpret schedules in the kube-controller-manager's local timezone (usually UTC).

Next 5 runs (your local time)

Calculating…

Job configuration

name, image, command, resources
Wrapped in ["/bin/sh", "-c", ...] so shell features like $VAR and $(cmd) work.
Leave set. Without a deadline, missed schedules can accumulate to 100 and stop the controller dead.
my-cronjob-cronjob.yaml
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 SUNSAT as case-insensitive aliases. Months accept JANDEC 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.

Key insight

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.

"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.