| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | kubectl 1.36.3 |
| Applies to | Any host with kubectl configured; Kubernetes v1.27 or later |
| Cert prep | CKA · CKAD |
| Lab environment | Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm |
| Privilege | Normal user (no sudo required on the workstation) |
| Scope | CronJob YAML, cron schedule syntax, timeZone, concurrencyPolicy, startingDeadlineSeconds, suspend and resume, manual Job runs, history limits, inspection, update, deletion, and common CronJob problems. Does not cover Job parallelism, backoffLimit depth, indexed Jobs, init or sidecar patterns, or external workflow schedulers. |
| Related guides | Choose a Kubernetes workload resource Kubernetes Pods and Pod Lifecycle Container command, args, and environment |
A CronJob creates Jobs on a recurring schedule from a jobTemplate. Each Job then creates the Pods that carry out the task, which is why periodic backups, reports, and cleanup belong here rather than under a Deployment. Work in the cron-lab namespace: apply report-cron to watch scheduled Jobs and read logs, add slow-forbid to observe Forbid during overlapping runs, then configure startingDeadlineSeconds, suspend and resume, history limits, and a one-off Job from the CronJob template.
What Is a Kubernetes CronJob?
A CronJob creates Kubernetes Jobs according to a recurring schedule. Each Job creates one or more Pods to perform the task.
CronJobs fit periodic work:
- Backups on a schedule
- Report generation
- Log or cache cleanup
- Maintenance scripts
A CronJob is not for continuously running applications. Long-running services belong under a Deployment or StatefulSet.
The controller chain looks like this:
CronJob → Job → Pod
For Job-level retries, parallelism, deadlines, and TTL cleanup on finished Jobs, see Kubernetes Jobs with examples.
Create a Kubernetes CronJob
Save this manifest as report-cron.yaml:
apiVersion: batch/v1
kind: CronJob
metadata:
name: report-cron
namespace: cron-lab
spec:
schedule: "* * * * *"
timeZone: "Asia/Kolkata"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: worker
image: busybox:1.36
command: ["sh", "-c", "echo Report run at $(date)"]| Field | Location | Purpose |
|---|---|---|
| Schedule | spec.schedule |
Defines when Kubernetes creates each Job |
| Job template | spec.jobTemplate |
Defines the Job configuration copied for every scheduled run |
| Pod template | spec.jobTemplate.spec.template |
Defines containers, commands, environment variables, volumes, and restartPolicy |
| Job controls | spec.jobTemplate.spec |
Holds fields such as backoffLimit, activeDeadlineSeconds, and ttlSecondsAfterFinished |
Keep a CronJob name at 52 characters or fewer because the controller appends 11 characters when generating Job names.
The schedule runs every minute so you can see a scheduled Job quickly in a lab. In production, use a less frequent expression such as */5 * * * * for every five minutes.
Create the namespace and apply the CronJob:
kubectl create namespace cron-labSample output:
namespace/cron-lab createdkubectl apply -f report-cron.yamlSample output:
cronjob.batch/report-cron createdList CronJobs and read the schedule columns:
kubectl get cronjobs -n cron-labSample output:
NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE
report-cron * * * * * Asia/Kolkata False 0 <none> 0sSCHEDULE shows the cron expression. TIMEZONE shows the IANA zone when spec.timeZone is set. SUSPEND is False while the CronJob is active. ACTIVE counts Jobs still running. LAST SCHEDULE updates after the controller creates a Job.
Wait until one Job reaches Complete, then press Ctrl+C:
kubectl get jobs -n cron-lab --watchCapture the newest generated Job name:
JOB_NAME=$(kubectl get jobs -n cron-lab --sort-by=.metadata.creationTimestamp -o custom-columns=NAME:.metadata.name --no-headers | tail -n 1)
echo "$JOB_NAME"Sample output (your suffix will differ):
report-cron-29750888Wait for completion before reading logs:
kubectl wait --for=condition=complete "job/$JOB_NAME" -n cron-lab --timeout=60sSample output:
job.batch/report-cron-29750888 condition metList the Pod for that Job:
kubectl get pods -n cron-lab -l "batch.kubernetes.io/job-name=$JOB_NAME"Sample output (Pod name suffix will differ):
NAME READY STATUS RESTARTS AGE
report-cron-29750888-vs76d 0/1 Completed 0 13sRead the task output:
kubectl logs -n cron-lab "job/$JOB_NAME"Sample output:
Report run at Sun Jul 26 08:08:01 UTC 2026The log line came from the Pod the CronJob's Job created.
Understand Kubernetes Cron Schedule Syntax
Kubernetes CronJobs use five fields:
┌───────────── minute (0–59)
│ ┌───────────── hour (0–23)
│ │ ┌───────────── day of month (1–31)
│ │ │ ┌───────────── month (1–12)
│ │ │ │ ┌───────────── day of week (0–6, Sunday = 0)
│ │ │ │ │
* * * * *Common schedules:
| Schedule | Meaning |
|---|---|
*/5 * * * * |
Every five minutes |
0 * * * * |
At the beginning of every hour |
0 2 * * * |
Daily at 02:00 |
0 2 * * 1 |
Every Monday at 02:00 |
0 0 1 * * |
On the first day of every month |
Macros such as @hourly, @daily, @weekly, @monthly, and @yearly are also supported. This section covers only what you need for Kubernetes CronJobs, not full Linux cron administration.
Configure the CronJob Time Zone
spec.timeZone sets which IANA time zone interprets spec.schedule:
spec:
schedule: "0 2 * * *"
timeZone: "Asia/Kolkata"- Use a valid IANA time-zone name such as
Asia/KolkataorAmerica/New_York Etc/UTCschedules in UTC- Without
timeZone, the kube-controller-manager local time zone applies - Do not put
TZ=orCRON_TZ=inside the schedule string
The TIMEZONE column in kubectl get cronjobs reflects the value you set on the CronJob.
Control Concurrent CronJob Runs
spec.concurrencyPolicy controls overlapping Jobs from the same CronJob:
| Policy | Behaviour |
|---|---|
Allow |
Overlapping Jobs may run (default) |
Forbid |
Skips a new run while a previous Job is still active |
Replace |
Terminates the active Job and starts the new run |
This example uses Forbid with a ninety-second task and an every-minute schedule. Save it as slow-forbid.yaml:
apiVersion: batch/v1
kind: CronJob
metadata:
name: slow-forbid
namespace: cron-lab
spec:
schedule: "* * * * *"
concurrencyPolicy: Forbid
jobTemplate:
metadata:
labels:
example: slow-forbid
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: worker
image: busybox:1.36
command:
- sh
- -c
- echo "start $(date)"; sleep 90; echo "done $(date)"Apply it:
kubectl apply -f slow-forbid.yamlSample output:
cronjob.batch/slow-forbid createdWatch its Jobs through at least two minute boundaries:
kubectl get jobs -n cron-lab -l example=slow-forbid --watchKeep watching until the active Job is more than 60 seconds old and no second Job appears, then press Ctrl+C. Forbid treats the overlapping occurrence as missed. Because slow-forbid does not set startingDeadlineSeconds, that missed occurrence has no deadline and may be created after the active Job finishes. Delete the CronJob while its first Job is still active to keep this demonstration limited to one Job.
Sample output (Job name suffix will differ):
NAME STATUS COMPLETIONS DURATION AGE
slow-forbid-29750889 Running 0/1 67s 67skubectl delete cronjob slow-forbid -n cron-labControl Missed Schedules
startingDeadlineSeconds limits how late Kubernetes may start a Job after a missed schedule. Runs that fall outside that window are skipped.
Patch report-cron to add a sixty-second deadline:
kubectl patch cronjob report-cron -n cron-lab --type=merge -p '{"spec":{"startingDeadlineSeconds":60}}'Sample output:
cronjob.batch/report-cron patchedVerify the stored value without depending on kubectl describe formatting:
kubectl get cronjob report-cron -n cron-lab -o jsonpath='{.spec.startingDeadlineSeconds}{"\n"}'Sample output:
60Avoid values below 10 seconds because the CronJob controller checks schedules approximately every 10 seconds, so a very short deadline can prevent the Job from being created.
Brief controller downtime or clock skew can cause missed schedules. Keep nodes time-synchronized and design tasks to tolerate occasional duplicate or missed runs.
Suspend and Resume a CronJob
Set suspend: true to stop creating new Jobs:
spec:
suspend: trueSuspending prevents new Jobs but does not stop Jobs that are already running. The schedule stays configured. Missed runs can matter when you resume a CronJob that was suspended for a long time.
Suspend with kubectl patch:
kubectl patch cronjob report-cron -n cron-lab -p '{"spec":{"suspend":true}}'Sample output:
cronjob.batch/report-cron patchedkubectl get cronjob report-cron -n cron-labSample output:
NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE
report-cron * * * * * Asia/Kolkata True 0 19s 72sResume scheduling:
kubectl patch cronjob report-cron -n cron-lab -p '{"spec":{"suspend":false}}'Sample output:
cronjob.batch/report-cron patchedSUSPEND returns to False. This CronJob now uses startingDeadlineSeconds: 60. After resuming, only a missed schedule that is no more than 60 seconds late remains eligible; older missed schedules are skipped.
startingDeadlineSeconds determines how late a missed execution can still begin, while suspend only prevents new Jobs during the suspended period.
Run a CronJob Manually
Create a one-time Job from the CronJob template when you need to test without waiting for the schedule:
kubectl create job --from=cronjob/report-cron manual-run-1 -n cron-labSample output:
job.batch/manual-run-1 createdWait for completion and read logs:
kubectl wait --for=condition=complete job/manual-run-1 -n cron-lab --timeout=60sSample output:
job.batch/manual-run-1 condition metkubectl logs -n cron-lab job/manual-run-1Sample output:
Report run at Sun Jul 26 08:08:15 UTC 2026manual-run-1 is a standalone Job copied from the CronJob template. It is not a scheduled Job owned by report-cron, so CronJob history limits do not manage it. The kubectl create job --from=cronjob/... syntax is current and officially supported.
Delete the test Job when finished:
kubectl delete job manual-run-1 -n cron-lab --wait=trueManage CronJob History
successfulJobsHistoryLimit and failedJobsHistoryLimit control how many finished Jobs Kubernetes keeps per CronJob. The documented defaults are three successful Jobs and one failed Job.
Patch report-cron to keep one successful and one failed Job:
kubectl patch cronjob report-cron -n cron-lab --type=merge -p '{"spec":{"successfulJobsHistoryLimit":1,"failedJobsHistoryLimit":1}}'Sample output:
cronjob.batch/report-cron patchedVerify both values:
kubectl get cronjob report-cron -n cron-lab -o jsonpath='{.spec.successfulJobsHistoryLimit}{" "}{.spec.failedJobsHistoryLimit}{"\n"}'Sample output:
1 1These limits apply to Job objects created by the CronJob, not to manually created Jobs. They prevent old Job metadata from accumulating.
For TTL-based cleanup on individual Jobs after they finish, set ttlSecondsAfterFinished inside jobTemplate.spec.
Manage and Inspect a Kubernetes CronJob
View Jobs and Logs
| Task | Command |
|---|---|
| List CronJobs | kubectl get cronjobs |
| Describe a CronJob | kubectl describe cronjob <name> |
| List created Jobs | kubectl get jobs |
| List Pods for a Job | kubectl get pods -l batch.kubernetes.io/job-name=<job-name> |
| View Job logs | kubectl logs job/<job-name> |
| Show last schedule metadata | kubectl get cronjob <name> -o yaml |
CronJob logs live on the Pods created by its Jobs. The CronJob object itself has no container logs.
Update the CronJob
Changes apply to Jobs created after the update. Existing Jobs and Pods keep their original specifications.
Edit the manifest and change schedule, the container image, command, or concurrencyPolicy, then apply:
kubectl apply -f report-cron.yamlVerify the CronJob spec:
kubectl get cronjob report-cron -n cron-labThe SCHEDULE and TIMEZONE columns reflect updates to those fields. Jobs that already exist are not retrofitted.
Delete the CronJob
Delete the CronJob and its managed Jobs:
kubectl delete cronjob report-cron -n cron-labCascading deletion removes Jobs the CronJob created. Suspend the CronJob instead when you may need the same schedule again without recreating the object.
Common Kubernetes CronJob Problems
| Symptom | Likely cause | Fix |
|---|---|---|
| CronJob not creating Jobs | Invalid schedule, suspend: true, expired startingDeadlineSeconds, invalid timeZone, or controller issue |
Check kubectl describe cronjob; on self-managed clusters verify kube-controller-manager, and on managed clusters check the provider's control-plane status |
| Job runs at unexpected local time | Wrong or missing timeZone |
Set a valid IANA name on spec.timeZone and confirm TIMEZONE in kubectl get cronjobs |
| Scheduled run skipped while a Job is active | concurrencyPolicy: Forbid |
Wait for the active Job to finish or change the policy |
| Active Job terminated when the next run starts | concurrencyPolicy: Replace |
Use Forbid or Allow if overlapping or queued runs are unacceptable |
| Overlapping Jobs from one CronJob | Default Allow policy or long-running tasks |
Set concurrencyPolicy: Forbid when overlaps are not allowed |
| Jobs start immediately after resume | Scheduled times were missed while suspended | Set a suitable startingDeadlineSeconds before resuming when stale runs should be skipped |
| Duplicate or missed execution | Controller timing or node clock skew | Design idempotent tasks; keep node clocks synchronized |
What's Next
- Kubernetes Liveness, Readiness and Startup Probes
- Kubernetes ConfigMap with Examples
- Kubernetes Secrets with Examples
References
Summary
You applied a batch/v1 CronJob with jobTemplate, set restartPolicy: OnFailure on the Pod template, and traced the chain from CronJob to Job to Pod. kubectl get cronjobs shows SCHEDULE, TIMEZONE, SUSPEND, ACTIVE, and LAST SCHEDULE together so you can see whether the controller is scheduling work.
timeZone interprets the schedule on the CronJob object; it does not set the container clock. concurrencyPolicy and startingDeadlineSeconds answer different overlap and lateness questions. suspend stops future Jobs without stopping ones already running. History limits and Job-level TTL from the Job guide control how long finished Job objects remain in the cluster.
Use a CronJob when the same task should run on a calendar or interval. Use a Job for one-time execution and a CronJob for recurring schedules. See Job versus CronJob for the focused comparison.

