Kubernetes CronJob runs overlap after a slow job exceeds its schedule because concurrencyPolicy defaults to Allow

2026-11-13 · 3 min read

Kubernetes CronJob runs overlap after a slow job exceeds its schedule because concurrencyPolicy defaults to Allow

Your nightly export starts at 01:00. At 02:00 it is still running, so the CronJob starts another copy. By 03:00 there are three exports writing to the same table, competing for the same API quota, and racing on the same temporary files. The schedule did not change; the job simply ran longer than its interval. The usual cause is that the Kubernetes CronJob concurrencyPolicy defaults to Allow, so the controller does not stop a new Job while the previous one is still active [link needed].

Why kubernetes cronjob concurrencyPolicy Allow causes overlap

The kubernetes cronjob concurrencyPolicy Allow default means each scheduled time can create a new Job even if an earlier Job from the same CronJob has not completed [link needed]. For short, independent jobs this is fine. For slow jobs, it turns a missed deadline into a pile-up: duplicate writes, doubled load, lock contention, and confusing logs.

The overlap is visible in the Job history. You will see multiple active Jobs from the same CronJob, often with start times one schedule interval apart. If the workload is not idempotent, the second run may duplicate records or corrupt shared state. If it is idempotent but expensive, it can still exhaust memory, connections, or external rate limits.

Fix it with Forbid or Replace

Use Forbid when only one run should be active and skipping a scheduled run is acceptable. Use Replace when the newest run should supersede a stale one; Kubernetes cancels the currently running Job and starts the new one [link needed]. Both are safer than leaving the default in place for a long-running job.

1. Inspect the current spec and recent Jobs:

```bash

kubectl get cronjob my-job -o yaml

kubectl get jobs

kubectl get pods

```

Confirm that overlap exists and identify how long runs normally take.

2. Choose the policy. Pick Forbid for exports, migrations, and singleton reconcilers where two copies are harmful. Pick Replace when an old run is useless once a new schedule fires.

3. Patch the CronJob:

```bash

kubectl patch cronjob my-job --type=merge \

-p '{"spec":{"concurrencyPolicy":"Forbid"}}'

```

4. Add a deadline so stale schedules do not accumulate indefinitely:

```yaml

spec:

concurrencyPolicy: Forbid

startingDeadlineSeconds: 300

```

With startingDeadlineSeconds, a missed run is skipped if it cannot start within the deadline [link needed].

5. Watch the next few schedule windows. Confirm there is at most one active Job, and confirm that skipped runs are expected rather than silent failures.

Verify the change and watch for missed schedules

After the patch, check kubectl get cronjob my-job -o yaml and confirm concurrencyPolicy is no longer Allow. Then inspect Jobs after two schedule intervals. You should see one active Job at a time, or a replaced Job when using Replace.

Do not stop at the YAML change. If Forbid skips too many runs, the job is effectively under-scheduled. Track duration against the interval and alert when a run approaches the next schedule. Also watch the CronJob’s conditions; the controller reports too many missed schedules when the count exceeds its limit [link needed].

When NOT to do this

Do not set Forbid if every scheduled run must execute and the job routinely exceeds its interval; you will just convert overlap into skipped work. Do not set Replace if cancellation can leave non-idempotent side effects half-finished.

Also, kubernetes cronjob concurrencyPolicy Allow is not wrong for jobs that are idempotent, cheap, and designed for parallel runs. And do not treat this policy as a distributed lock across clusters or separate CronJobs; it only governs Jobs created by the same CronJob [link needed].

Checklist