CronJob Patterns
Summary
- A CronJob is a controller that creates a Job on a cron schedule, so all Job semantics still apply to each run.
- Insight: the tricky parts are not the schedule string but timezone handling, overlap control, and what happens when a run is missed.
- Key Concepts: schedule, timeZone, concurrencyPolicy, startingDeadlineSeconds, history limits, suspend.
- When to Use: nightly reports, periodic backups, cache warmers, retention cleanups, and scheduled syncs.
- Limitations: CronJobs give at-most-once-ish semantics, not guaranteed exactly-once execution, so make jobs idempotent.
Recipe
Define a CronJob with an explicit timezone, a concurrency policy, and history limits.
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-report
spec:
schedule: "0 2 * * *"
timeZone: "America/New_York"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 120
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
containers:
- name: report
image: myrepo/report:3.2.0Apply it and confirm the schedule is parsed.
kubectl apply -f cronjob.yaml
kubectl get cronjob nightly-reportWorking Example
This CronJob runs a database backup every six hours, refuses overlap, and keeps a bounded history.
apiVersion: batch/v1
kind: CronJob
metadata:
name: pg-backup
spec:
schedule: "0 */6 * * *"
timeZone: "Etc/UTC"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 5
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 3
activeDeadlineSeconds: 3600
ttlSecondsAfterFinished: 172800
template:
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: backup
image: myrepo/pg-backup:1.8.0
env:
- name: PGHOST
value: postgres.data.svc.cluster.local
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
memory: "512Mi"Inspect what the CronJob has produced.
kubectl get jobs -l app.kubernetes.io/name=pg-backup
kubectl get cronjob pg-backup -o wide
kubectl logs job/pg-backup-28734120Deep Dive
The schedule field
The schedule uses standard five-field cron syntax: minute, hour, day-of-month, month, day-of-week. 0 2 * * * means 02:00 daily.
Kubernetes also accepts the shorthand macros @hourly, @daily, @weekly, @monthly, and @yearly. These are convenient but hide the exact minute, so prefer explicit fields for anything important.
Timezones
Historically CronJobs ran in the kube-controller-manager's timezone, which is usually UTC. That surprised teams whose "2 AM" was actually 2 AM UTC.
The timeZone field fixes this. It takes an IANA name such as America/New_York or Etc/UTC, and the schedule is evaluated in that zone.
Be deliberate about daylight saving time. During a spring-forward transition a 02:30 schedule may be skipped, and during fall-back it may not run twice; keep critical jobs off the ambiguous hour.
Concurrency policy
concurrencyPolicy controls what happens when a new run is due while the previous run is still going.
Allow is the default and lets runs overlap. Use it only when runs are truly independent and safe to stack.
Forbid skips the new run if the previous Job is still active. This is the right choice for backups and reports that must not run twice at once.
Replace cancels the still-running Job and starts a fresh one. Use it when only the latest run matters, such as a cache refresh.
Missed schedules and startingDeadlineSeconds
If the controller was down or the cluster was busy, a scheduled time can pass without a Job being created.
startingDeadlineSeconds bounds how late a run may start. If more than that many seconds have elapsed since the scheduled time, the run is counted as missed and skipped.
Without this field, the controller looks back and, if it counts more than 100 missed schedules, stops scheduling entirely and logs an error. Setting a modest deadline like 120 to 300 seconds avoids that failure mode.
History limits and suspend
successfulJobsHistoryLimit and failedJobsHistoryLimit cap how many finished Jobs are retained, defaulting to 3 and 1. Keeping a few of each aids debugging without cluttering the namespace.
suspend: true stops new Jobs from being created without deleting the CronJob. It is the clean way to pause a schedule during an incident or maintenance window.
Gotchas
Overlap surprises are common. A slow Job under concurrencyPolicy: Allow can spawn stacked runs that exhaust resources; switch to Forbid or Replace.
The 100-missed-schedule cliff bites clusters that were paused or scaled to zero. Always set startingDeadlineSeconds so a long outage does not silently disable the CronJob.
CronJobs do not guarantee exactly-once execution. A run can be skipped or, in rare edge cases, created twice, so the job body must be idempotent.
Timezone assumptions cause off-by-hours bugs. Always set timeZone explicitly rather than relying on the controller's zone.
Forgetting TTL on the jobTemplate leaves finished Jobs piling up beyond the history limits' Pods, so set ttlSecondsAfterFinished inside the template too.
Alternatives
For event-driven rather than time-driven work, a queue plus a Deployment consumer or KEDA scaling on queue depth fits better than a CronJob.
For complex multi-step pipelines with dependencies, a workflow engine such as Argo Workflows models DAGs that plain CronJobs cannot express.
For running the same schedule as part of GitOps, wrap the CronJob in a Helm chart or Kustomize base and let Argo CD reconcile it, keeping the manifest declarative.
For sub-minute frequencies, note that cron cannot schedule faster than once per minute; use a long-running loop in a Deployment instead.
FAQs
Why did my CronJob stop creating Jobs? It likely hit the 100-missed-schedule limit after an outage. Set startingDeadlineSeconds and check controller-manager logs.
How do I run a CronJob's work right now for testing? Create a one-off Job from it with kubectl create job --from=cronjob/pg-backup manual-run.
Does timeZone handle daylight saving automatically? Yes, it uses IANA rules, but schedules on the transition hour can be skipped or ambiguous, so avoid that hour.
What is the default concurrencyPolicy? Allow, which permits overlapping runs. Set Forbid or Replace when overlap is unsafe.
How many finished Jobs are kept? By default 3 successful and 1 failed, tunable with the two history limit fields.
Can I pause a schedule without deleting it? Yes, patch spec.suspend to true, then back to false to resume.
Related
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).