Kubernetes
cron jobs
scheduling
container orchestration
workload management

Avoid multiple cron jobs running for one cron execution point in Kubernetes

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Overlapping Kubernetes CronJob executions can lead to duplicate processing, conflicting writes, and unnecessary cluster load. The scheduler will happily start new jobs unless you tell it otherwise, so preventing overlap requires explicit policy. In production, the right solution combines CronJob configuration with workload-level idempotency and reasonable runtime limits.

Why Multiple Runs Happen

The most common cause is simple: the previous run is still active when the next schedule fires.

Typical reasons:

  • The job takes longer than its schedule interval.
  • Retries keep the job alive.
  • External services are slow.
  • Manual reruns overlap with the regular schedule.

Kubernetes is not misbehaving in this case. It is following the schedule you defined.

concurrencyPolicy Is the First Control

The main built-in safeguard is concurrencyPolicy.

yaml
1apiVersion: batch/v1
2kind: CronJob
3metadata:
4  name: reports
5spec:
6  schedule: "*/5 * * * *"
7  concurrencyPolicy: Forbid
8  jobTemplate:
9    spec:
10      template:
11        spec:
12          restartPolicy: Never
13          containers:
14            - name: worker
15              image: myorg/reports:1.0.0
16              args: ["run"]

The available values are:

  • 'Allow, which permits overlap.'
  • 'Forbid, which skips the new run if a previous one is still active.'
  • 'Replace, which terminates the older run and starts the new one.'

For most stateful workloads, Forbid is the safest choice.

Add Runtime Boundaries

Even with Forbid, you should limit how long a job can keep the schedule blocked.

yaml
1apiVersion: batch/v1
2kind: CronJob
3metadata:
4  name: nightly-reconcile
5spec:
6  schedule: "0 2 * * *"
7  concurrencyPolicy: Forbid
8  startingDeadlineSeconds: 300
9  jobTemplate:
10    spec:
11      backoffLimit: 1
12      activeDeadlineSeconds: 1800
13      template:
14        spec:
15          restartPolicy: Never
16          containers:
17            - name: reconcile
18              image: myorg/reconcile:2.4.0

These settings help with two problems:

  • Very late starts after controller disruption.
  • Jobs that keep retrying and blocking future runs.

Know When Replace Is Better

Not every job wants Forbid. Some jobs only care about the latest schedule point.

Examples:

  • Periodic cache refresh.
  • Polling-style metadata sync.
  • Non-critical aggregation where old work is obsolete once a new window starts.

In those cases, Replace may be the better operational choice because it preserves “latest only” semantics instead of letting an old run consume time.

Idempotency Still Matters

Scheduler policy reduces overlap, but it does not eliminate all duplicate-execution scenarios. Manual restarts, cluster recovery behavior, or application-level retries can still cause the same logical work to happen more than once.

That is why the job itself should be idempotent where possible:

  • Use unique execution markers.
  • Upsert rather than blindly insert.
  • Make writes atomic.
  • Check whether a work unit was already processed.

Cluster configuration is not a substitute for safe workload design.

When External Locking Is Worth It

For high-impact jobs, you may still want an external lock in Redis, Postgres, or another durable store.

Minimal Redis-style example in Python:

python
1import time
2
3def run_with_lock(lock_client, key="nightly-reconcile-lock", ttl=1800):
4    # setnx-like semantics: acquire only if lock does not exist
5    acquired = lock_client.set(key, "1", nx=True, ex=ttl)
6    if not acquired:
7        print("another execution already owns the lock")
8        return
9
10    try:
11        print("running job")
12        time.sleep(1)
13    finally:
14        lock_client.delete(key)

This should be used carefully. Complex locking schemes create their own failure modes if TTL and cleanup are not thought through.

Monitor for Near-Overlap, Not Just Failures

Do not wait until duplicates cause incidents. Track:

  • Active job count.
  • Job runtime percentiles.
  • Missed schedule count.
  • Retry frequency.

If average runtime is approaching the schedule interval, you already have an overlap risk even if no duplicate execution has happened yet.

Common Pitfalls

  • Leaving concurrencyPolicy at the default Allow for non-idempotent jobs. Fix by explicitly setting Forbid or Replace.
  • Scheduling jobs more frequently than they can realistically finish. Fix by matching interval to runtime budget.
  • Assuming scheduler policy alone prevents all duplicate work. Fix by making the job logic idempotent.
  • Letting retries run for too long. Fix by setting backoffLimit and activeDeadlineSeconds.
  • Adding distributed locks without a clear failure and TTL model. Fix by keeping lock design simple and operationally testable.

Summary

  • CronJob overlap happens when schedule frequency and job runtime are misaligned.
  • 'concurrencyPolicy is the primary Kubernetes control for overlapping runs.'
  • 'Forbid is usually safest for stateful workloads, while Replace is better for latest-only jobs.'
  • Runtime deadlines and retry limits keep schedules predictable.
  • Idempotent job logic is still required because scheduler settings are not the whole reliability story.

Course illustration
Course illustration

All Rights Reserved.