CronJob Schedules and concurrencyPolicy
A CronJob creates a Job on a schedule, and the interesting question is what it does when the previous Job is still running. `concurrencyPolicy` answers it, and the default is to let them overlap. This runs a two-and-a-half minute job on a one-minute schedule with `Forbid` set, so the skip is observable, and covers the history limits and `suspend` alongside it.
Application Design and Build Guide 11 of 44 Intermediate
- Kubernetes1.36.4
- Runtimecontainerd 2.2.6
- CNICalico v3.32.1
- TimeAbout 15 min
- Reviewed23 August 2026
Written against the versions above. The Job names a CronJob generates end in a number - `tick-29790909` - which is the scheduled time in minutes since the epoch, not a random suffix. Two Jobs from consecutive minutes therefore have consecutive numbers, which makes the gap left by a skipped run visible in the listing.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA1001 | 192.168.0.175 | Ubuntu 26.04 LTS | Control Plane Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE01 | 192.168.0.176 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE02 | 192.168.0.177 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE03 | 192.168.0.178 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- A cluster and kubectl. This guide waits for real schedule ticks, so it takes a few minutes of wall-clock time.
- The session creates namespace
ckad-cron, a CronJobtickrunning every minute, and a CronJobslowwhose job takes 150 seconds on a 60-second schedule.
-
A Job every minute
kubectl create cronjobgenerates this in one line - nojobTemplatenesting typed by hand. After seventy-five seconds there are Jobs in the namespace, and the CronJob records when it last fired:SCHEDULE LAST SUCCESSHIST FAILHIST */1 * * * * 2026-08-23T03:...Z 3 1successfulJobsHistoryLimit: 3,failedJobsHistoryLimit: 1- both defaults, and both are why a CronJob that has run for a week has only three Jobs to show for it. If you are looking for the run from an hour ago, it has been garbage collected.The logs of every run at once, with the Job name prefixed, is the query worth knowing:
kubectl logs -l batch.kubernetes.io/job-name --tail=1 --prefix=truebash Example session kubectl create namespace ckad-cronnamespace/ckad-cron createdkubectl -n ckad-cron create cronjob tick --image=busybox:1.36 --schedule='*/1 * * * *' -- /bin/sh -c 'date +%H:%M:%S'cronjob.batch/tick createdkubectl -n ckad-cron get cronjob tickNAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGEtick */1 * * * * <none> False 0 <none> 0ssleep 75; kubectl -n ckad-cron get jobsNAME STATUS COMPLETIONS DURATION AGEtick-29790908 Complete 1/1 3s 72stick-29790909 Complete 1/1 2s 12skubectl -n ckad-cron get cronjob tick -o 'custom-columns=SCHEDULE:.spec.schedule,LAST:.status.lastScheduleTime,SUCCESSHIST:.spec.successfulJobsHistoryLimit,FAILHIST:.spec.failedJobsHistoryLimit'SCHEDULE LAST SUCCESSHIST FAILHIST*/1 * * * * 2026-08-23T03:09:00Z 3 1Expected resultOne Job per minute, and the two history limits at their defaults.
Success conditionYou have a CronJob and know why old runs disappear.
-
What happens when a run overruns
slowsleeps 150 seconds on a*/1 * * * *schedule, so by the time the second tick comes due the first is still going. WithconcurrencyPolicy: Forbid:JOB ACTIVE SUCCEEDED slow-29790910 1 <none> tick-29790909 <none> 1 tick-29790910 <none> 1 tick-29790911 <none> 1One
slowJob across two ticks.tickproduced ...909, ...910 and ...911;slowproduced only ...910. The ...911 run was due and was skipped.POLICY ACTIVE LAST Forbid slow-29790910 2026-08-23T03:10:00ZThe skip is silent -
describe cronjobrecorded no event for it. Nothing tells you a run did not happen except the gap in the numbering, which is worth knowing before you rely on a schedule for something that matters.The three policies:
Allow(default) - runs overlap. Fine for idempotent work, bad for anything that writes.Forbid- skip the new run, as here.Replace- kill the running Job and start the new one.
bash Example session sleep 140; kubectl -n ckad-cron get jobs -l app!=none -o 'custom-columns=JOB:.metadata.name,ACTIVE:.status.active,SUCCEEDED:.status.succeeded'JOB ACTIVE SUCCEEDEDslow-29790910 1 <none>tick-29790909 <none> 1tick-29790910 <none> 1tick-29790911 <none> 1kubectl -n ckad-cron get cronjob slow -o 'custom-columns=POLICY:.spec.concurrencyPolicy,ACTIVE:.status.active[*].name,LAST:.status.lastScheduleTime'POLICY ACTIVE LASTForbid slow-29790910 2026-08-23T03:10:00ZExpected resultOne active
slowJob over two schedule ticks.Success conditionYou can choose a concurrency policy and know the skip will be silent.
-
Suspend it
NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE tick */1 * * * * <none> True 0 34s 3m38sSUSPEND Trueand nothing further is created. Already-running Jobs are left alone - suspend stops *scheduling*, it does not stop work in flight.This is the correct answer to "stop this CronJob" on the exam and in production, and it is much better than deleting it, because the object and its history stay. Unsuspend with the same patch and
false.Note
TIMEZONE: withoutspec.timeZonethe schedule is interpreted in the controller's time zone, which on most clusters is UTC and is very rarely what a business schedule means.bash Example session kubectl -n ckad-cron patch cronjob tick --type=merge -p '{"spec":{"suspend":true}}'cronjob.batch/tick patchedkubectl -n ckad-cron get cronjob tick -o 'custom-columns=SCHEDULE:.spec.schedule,LAST:.status.lastScheduleTime,SUCCESSHIST:.spec.successfulJobsHistoryLimit,FAILHIST:.spec.failedJobsHistoryLimit'SCHEDULE LAST SUCCESSHIST FAILHIST*/1 * * * * 2026-08-23T03:09:00Z 3 1kubectl -n ckad-cron get cronjob tick -o jsonpath='{.spec.suspend}'truekubectl delete namespace ckad-cron --wait=falsenamespace "ckad-cron" deletedExpected result
SUSPEND Trueand no further Jobs.Success conditionYou can stop a schedule without destroying it.
Troubleshooting
A CronJob's older runs have disappeared.
Why:
successfulJobsHistoryLimitdefaults to 3 andfailedJobsHistoryLimitto 1.Fix:Raise them, or ship the logs somewhere. Do not rely on the Jobs being there.
Runs are overlapping and corrupting each other.
Why:
concurrencyPolicydefaults toAllow.Fix:Set
Forbidto skip, orReplaceto cancel the running one.A scheduled run silently did not happen.
Why:
Forbidskipped it because the previous run was still active, or the controller was down paststartingDeadlineSeconds.Fix:Compare the Job name suffixes - they are consecutive scheduled minutes, so a gap is a skip. No event is emitted.
The job runs at the wrong time of day.
Why: No
spec.timeZone, so the schedule uses the controller's zone - usually UTC.Fix:Set
spec.timeZone: "Europe/London"or similar.