Kubernetes CronJobs with Examples

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:

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:

bash
kubectl create namespace cron-lab

Sample output:

output
namespace/cron-lab created
bash
kubectl apply -f report-cron.yaml

Sample output:

output
cronjob.batch/report-cron created

List CronJobs and read the schedule columns:

bash
kubectl get cronjobs -n cron-lab

Sample output:

output
NAME          SCHEDULE    TIMEZONE       SUSPEND   ACTIVE   LAST SCHEDULE   AGE
report-cron   * * * * *   Asia/Kolkata   False     0        <none>          0s

SCHEDULE 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:

bash
kubectl get jobs -n cron-lab --watch

Capture the newest generated Job name:

bash
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):

output
report-cron-29750888

Wait for completion before reading logs:

bash
kubectl wait --for=condition=complete "job/$JOB_NAME" -n cron-lab --timeout=60s

Sample output:

output
job.batch/report-cron-29750888 condition met

List the Pod for that Job:

bash
kubectl get pods -n cron-lab -l "batch.kubernetes.io/job-name=$JOB_NAME"

Sample output (Pod name suffix will differ):

output
NAME                         READY   STATUS      RESTARTS   AGE
report-cron-29750888-vs76d   0/1     Completed   0          13s

Read the task output:

bash
kubectl logs -n cron-lab "job/$JOB_NAME"

Sample output:

output
Report run at Sun Jul 26 08:08:01 UTC 2026

The log line came from the Pod the CronJob's Job created.


Understand Kubernetes Cron Schedule Syntax

Kubernetes CronJobs use five fields:

text
┌───────────── 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:

yaml
spec:
  schedule: "0 2 * * *"
  timeZone: "Asia/Kolkata"
  • Use a valid IANA time-zone name such as Asia/Kolkata or America/New_York
  • Etc/UTC schedules in UTC
  • Without timeZone, the kube-controller-manager local time zone applies
  • Do not put TZ= or CRON_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:

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:

bash
kubectl apply -f slow-forbid.yaml

Sample output:

output
cronjob.batch/slow-forbid created

Watch its Jobs through at least two minute boundaries:

bash
kubectl get jobs -n cron-lab -l example=slow-forbid --watch

Keep 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):

output
NAME                   STATUS    COMPLETIONS   DURATION   AGE
slow-forbid-29750889   Running   0/1           67s        67s
bash
kubectl delete cronjob slow-forbid -n cron-lab

Control 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:

bash
kubectl patch cronjob report-cron -n cron-lab --type=merge -p '{"spec":{"startingDeadlineSeconds":60}}'

Sample output:

output
cronjob.batch/report-cron patched

Verify the stored value without depending on kubectl describe formatting:

bash
kubectl get cronjob report-cron -n cron-lab -o jsonpath='{.spec.startingDeadlineSeconds}{"\n"}'

Sample output:

output
60

Avoid 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:

yaml
spec:
  suspend: true

Suspending 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:

bash
kubectl patch cronjob report-cron -n cron-lab -p '{"spec":{"suspend":true}}'

Sample output:

output
cronjob.batch/report-cron patched
bash
kubectl get cronjob report-cron -n cron-lab

Sample output:

output
NAME          SCHEDULE    TIMEZONE       SUSPEND   ACTIVE   LAST SCHEDULE   AGE
report-cron   * * * * *   Asia/Kolkata   True      0        19s             72s

Resume scheduling:

bash
kubectl patch cronjob report-cron -n cron-lab -p '{"spec":{"suspend":false}}'

Sample output:

output
cronjob.batch/report-cron patched

SUSPEND 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:

bash
kubectl create job --from=cronjob/report-cron manual-run-1 -n cron-lab

Sample output:

output
job.batch/manual-run-1 created

Wait for completion and read logs:

bash
kubectl wait --for=condition=complete job/manual-run-1 -n cron-lab --timeout=60s

Sample output:

output
job.batch/manual-run-1 condition met
bash
kubectl logs -n cron-lab job/manual-run-1

Sample output:

output
Report run at Sun Jul 26 08:08:15 UTC 2026

manual-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:

bash
kubectl delete job manual-run-1 -n cron-lab --wait=true

Manage 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:

bash
kubectl patch cronjob report-cron -n cron-lab --type=merge -p '{"spec":{"successfulJobsHistoryLimit":1,"failedJobsHistoryLimit":1}}'

Sample output:

output
cronjob.batch/report-cron patched

Verify both values:

bash
kubectl get cronjob report-cron -n cron-lab -o jsonpath='{.spec.successfulJobsHistoryLimit}{" "}{.spec.failedJobsHistoryLimit}{"\n"}'

Sample output:

output
1 1

These 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:

bash
kubectl apply -f report-cron.yaml

Verify the CronJob spec:

bash
kubectl get cronjob report-cron -n cron-lab

The 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:

bash
kubectl delete cronjob report-cron -n cron-lab

Cascading 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


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.


Frequently Asked Questions

1. What is the difference between a Kubernetes CronJob and a Job?

A Job runs a finite task once. A CronJob creates Jobs on a recurring schedule from a jobTemplate. Use a Job for one-off work and a CronJob for periodic backups, reports, and cleanup.

2. Why is my CronJob not creating Jobs?

Check whether suspend is true, the schedule expression is valid, timeZone is a valid IANA name, and startingDeadlineSeconds has not expired a missed run. Inspect CronJob Events with kubectl describe cronjob. On a self-managed cluster, verify kube-controller-manager health; on a managed cluster, check the provider's control-plane status.

3. Why does my CronJob skip a scheduled run?

With concurrencyPolicy Forbid, an active Job blocks the next scheduled run. startingDeadlineSeconds can also skip runs that start too late after a missed schedule. Suspend prevents all new Jobs until you resume the CronJob.

4. How do I run a CronJob immediately without waiting for the schedule?

Create a one-time Job from the CronJob template with kubectl create job --from=cronjob/ . Monitor that Job and read its Pod logs, then delete the test Job when finished.

5. Does timeZone change the container clock inside the Pod?

No. spec.timeZone only controls how the CronJob controller interprets the schedule. It does not change the clock or time-zone configuration inside the container. Configure the application or container image separately when it must display or process dates in another time zone.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)