Back to Blog
Infrastructure12 min read

Kubernetes CronJob: Complete Guide with Examples

Kubernetes CronJobs let you run batch workloads on a repeating schedule inside your cluster. This guide covers everything from the basic YAML spec to production-grade monitoring, with copy-paste examples you can deploy today.

What is a Kubernetes CronJob?

A Kubernetes CronJob is a built-in resource that creates Job objects on a repeating schedule. Each time the schedule triggers, Kubernetes spins up one or more Pods, runs your container to completion, and records the result. It is the Kubernetes-native equivalent of the traditional Unix cron daemon, but designed for containerized, distributed environments.

CronJobs were promoted to stable (GA) in Kubernetes 1.21 and are part of the batch/v1 API group. Common use cases include database backups, log rotation, report generation, cache warming, certificate renewal, and data pipeline steps.

The CronJob YAML Spec

Here is the minimal YAML you need to create a CronJob. Every field is annotated:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: my-scheduled-task
  namespace: default
spec:
  schedule: "0 2 * * *"              # Run daily at 2:00 AM
  timeZone: "Europe/London"           # Optional (K8s 1.27+)
  concurrencyPolicy: Forbid           # Prevent overlapping runs
  startingDeadlineSeconds: 200        # Max seconds late before skip
  successfulJobsHistoryLimit: 3       # Keep last 3 successful Jobs
  failedJobsHistoryLimit: 5           # Keep last 5 failed Jobs
  suspend: false                      # Set true to pause
  jobTemplate:
    spec:
      backoffLimit: 2                 # Retry failed Pods up to 2x
      activeDeadlineSeconds: 600      # Kill Job after 10 minutes
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: task
              image: my-registry/backup:1.4.0
              command: ["/bin/sh", "-c"]
              args: ["python /app/backup.py --full"]
              resources:
                requests:
                  cpu: "100m"
                  memory: "128Mi"
                limits:
                  cpu: "500m"
                  memory: "512Mi"
              env:
                - name: DB_HOST
                  valueFrom:
                    secretKeyRef:
                      name: db-credentials
                      key: host

Apply it with kubectl apply -f cronjob.yaml and Kubernetes will start creating Jobs according to the schedule. You can verify with kubectl get cronjobs.

Schedule Syntax

Kubernetes CronJobs use the standard five-field cron expression format:

┌──────────── minute (0-59)
│ ┌────────── hour (0-23)
│ │ ┌──────── day of month (1-31)
│ │ │ ┌────── month (1-12)
│ │ │ │ ┌──── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *

Kubernetes also supports the following predefined macros:

MacroEquivalentDescription
@yearly0 0 1 1 *Once a year, midnight Jan 1
@monthly0 0 1 * *Once a month, midnight 1st
@weekly0 0 * * 0Once a week, midnight Sunday
@daily0 0 * * *Once a day at midnight
@hourly0 * * * *Once an hour, on the hour

Need help building expressions? Our Cron Expression Generator lets you construct schedules visually and preview upcoming execution times.

Concurrency Policies Explained

The concurrencyPolicy field controls what happens when a new Job is due but the previous one is still running. This is one of the most important settings to get right in production.

PolicyBehaviorUse When
AllowNew Job runs alongside existing one (default)Jobs are idempotent and independent
ForbidNew Job is skipped if previous is still runningDatabase operations, backups, anything with locks
ReplaceRunning Job is terminated, new one startsCache refresh where only the latest run matters

For most production workloads, Forbid is the safest choice. It prevents resource buildup from overlapping Jobs and avoids race conditions when your task modifies shared state.

Key Configuration Fields

Beyond the schedule and concurrency policy, these fields significantly affect how your CronJob behaves in production:

startingDeadlineSeconds

If the CronJob controller misses a scheduled time (due to controller restart, high cluster load, etc.), this field defines how many seconds late a Job can still be created. If the delay exceeds this value, the run is skipped entirely.

spec:
  startingDeadlineSeconds: 300   # Allow up to 5 minutes late

Without this field, Kubernetes counts missed schedules and will refuse to start the CronJob if more than 100 consecutive runs were missed. Setting a reasonable deadline (like 200-300 seconds) prevents this scenario.

successfulJobsHistoryLimit / failedJobsHistoryLimit

These control how many completed Job objects Kubernetes retains. Defaults are 3 for successful and 1 for failed. In production, increase failedJobsHistoryLimit to at least 5 so you can inspect failures before they are garbage collected.

spec:
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 5

backoffLimit and activeDeadlineSeconds

These are set on the jobTemplate.spec, not the CronJob spec itself. backoffLimit controls how many times a failed Pod is retried (default: 6). activeDeadlineSeconds sets a hard timeout for the entire Job, after which Kubernetes kills all running Pods.

jobTemplate:
  spec:
    backoffLimit: 2                # Retry failed Pods twice
    activeDeadlineSeconds: 3600    # Hard kill after 1 hour

timeZone (Kubernetes 1.27+)

Before Kubernetes 1.27, CronJob schedules always used the kube-controller-manager's timezone (usually UTC). The timeZone field lets you specify an IANA timezone string directly, which eliminates manual offset calculations.

spec:
  schedule: "0 9 * * 1-5"
  timeZone: "America/New_York"    # 9 AM Eastern, weekdays

Real-World Examples

Example 1: Nightly Database Backup

This CronJob runs a PostgreSQL backup every night at 2 AM UTC, uploads the dump to S3, and retains the last 7 days of backup history.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: postgres-backup-nightly
spec:
  schedule: "0 2 * * *"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 300
  successfulJobsHistoryLimit: 7
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 1
      activeDeadlineSeconds: 1800
      template:
        spec:
          restartPolicy: Never
          serviceAccountName: backup-sa
          containers:
            - name: pg-backup
              image: myregistry/pg-backup:2.1.0
              command: ["/bin/sh", "-c"]
              args:
                - |
                  TIMESTAMP=$(date +%Y%m%d_%H%M%S)
                  FILENAME="db_backup_${TIMESTAMP}.sql.gz"
                  pg_dump -h $DB_HOST -U $DB_USER $DB_NAME | gzip > /tmp/$FILENAME
                  aws s3 cp /tmp/$FILENAME s3://my-backups/postgres/$FILENAME
                  echo "Backup complete: $FILENAME"
              envFrom:
                - secretRef:
                    name: postgres-credentials
                - secretRef:
                    name: aws-credentials
              resources:
                requests:
                  cpu: "250m"
                  memory: "256Mi"
                limits:
                  cpu: "1"
                  memory: "1Gi"

Example 2: Hourly Cache Cleanup

This lightweight CronJob hits an internal API endpoint every hour to purge expired cache entries. It uses Replace concurrency because only the latest run matters.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: cache-cleanup-hourly
spec:
  schedule: "0 * * * *"
  concurrencyPolicy: Replace
  startingDeadlineSeconds: 120
  jobTemplate:
    spec:
      backoffLimit: 3
      activeDeadlineSeconds: 300
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: cleanup
              image: curlimages/curl:8.5.0
              command: ["/bin/sh", "-c"]
              args:
                - |
                  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
                    -X POST \
                    -H "Authorization: Bearer ${API_TOKEN}" \
                    http://app-service.default.svc:8080/api/cache/purge-expired)
                  if [ "$HTTP_CODE" != "200" ]; then
                    echo "Cache purge failed with HTTP $HTTP_CODE"
                    exit 1
                  fi
                  echo "Cache purge successful"
              env:
                - name: API_TOKEN
                  valueFrom:
                    secretKeyRef:
                      name: internal-api-token
                      key: token
              resources:
                requests:
                  cpu: "50m"
                  memory: "32Mi"
                limits:
                  cpu: "100m"
                  memory: "64Mi"

Example 3: Weekly Report Generation

Generates a PDF report every Monday at 7 AM and sends it via email. Uses a PersistentVolumeClaim for temporary storage.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: weekly-report
spec:
  schedule: "0 7 * * 1"
  timeZone: "Europe/Berlin"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      backoffLimit: 2
      activeDeadlineSeconds: 900
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: report
              image: myregistry/report-generator:3.0.1
              args: ["--type=weekly", "--format=pdf", "--send-email"]
              envFrom:
                - configMapRef:
                    name: report-config
                - secretRef:
                    name: smtp-credentials
              volumeMounts:
                - name: tmp-storage
                  mountPath: /tmp/reports
              resources:
                requests:
                  cpu: "500m"
                  memory: "512Mi"
                limits:
                  cpu: "2"
                  memory: "2Gi"
          volumes:
            - name: tmp-storage
              emptyDir:
                sizeLimit: "1Gi"

Monitoring Kubernetes CronJobs

CronJobs run silently by default. Without monitoring, a failed backup could go unnoticed for days. Here are the key strategies to keep your scheduled workloads observable.

kubectl Commands

# List all CronJobs and their last schedule time
kubectl get cronjobs

# See Jobs created by a CronJob
kubectl get jobs --selector=job-name=postgres-backup-nightly

# Check Pod logs for the latest run
kubectl logs job/postgres-backup-nightly-28467320

# Describe CronJob for events and status
kubectl describe cronjob postgres-backup-nightly

# Manually trigger a CronJob run (for testing)
kubectl create job --from=cronjob/postgres-backup-nightly manual-test-01

Prometheus Metrics

If you run kube-state-metrics (most clusters do), you get CronJob metrics out of the box:

MetricWhat It Tells You
kube_cronjob_status_last_schedule_timeWhen the CronJob last triggered
kube_cronjob_status_activeNumber of currently running Jobs
kube_job_status_succeededWhether a Job completed successfully
kube_job_status_failedWhether a Job failed

A useful Prometheus alert rule to detect stale CronJobs:

# Alert if a CronJob hasn't triggered in 26 hours (daily job)
- alert: CronJobMissedSchedule
  expr: |
    time() - kube_cronjob_status_last_schedule_time > 93600
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "CronJob {{ $labels.cronjob }} missed schedule"
    description: "No Job has been created for over 26 hours."

Common Issues and Fixes

CronJob stops creating Jobs after missed schedules

If the controller misses more than 100 consecutive schedule times, it stops creating Jobs entirely. Fix: Always set startingDeadlineSeconds. This changes the counting window so Kubernetes only looks at missed schedules within that window, not all of history.

Jobs pile up and consume cluster resources

This happens when concurrencyPolicy: Allow (the default) is used and Jobs take longer than the schedule interval. Fix: Set concurrencyPolicy: Forbid and add activeDeadlineSeconds to kill Jobs that run too long.

Pod starts but exits with ImagePullBackOff

The image name or tag is wrong, or the imagePullSecret is missing/expired. Fix: Verify the image exists with docker pull, check that imagePullSecrets is set in the Pod spec, and ensure the secret hasn't expired.

Job completes but shows status "Failed"

Your container might exit with a non-zero exit code even on success. Fix: Ensure your script ends with exit 0 on success and non-zero only on genuine failure. Check logs with kubectl logs.

CronJob runs at the wrong time

The kube-controller-manager uses its own timezone (usually UTC) for schedule evaluation. Fix: Use the timeZone field (K8s 1.27+) or manually convert your schedule to UTC.

Tired of debugging CronJob YAML?

CronJobPro schedules HTTP requests externally, so you get monitoring, retries, and alerts without touching Kubernetes manifests. Many teams use it alongside K8s CronJobs for tasks that call HTTP endpoints.

Try CronJobPro Free

When to Use an External Scheduler Instead

Kubernetes CronJobs are powerful for in-cluster batch workloads, but they have limitations that become painful as your operations grow:

  • No built-in alerting. You need to wire up Prometheus, Alertmanager, and a notification channel yourself. An external scheduler like CronJobPro sends email, Slack, or webhook alerts out of the box.
  • No execution dashboard. kubectl output is useful but not a monitoring dashboard. CronJobPro provides a visual timeline of every execution with status codes, response times, and response bodies.
  • HTTP endpoints only need HTTP calls. If your scheduled task is an API endpoint (like /api/daily-cleanup), running a full Pod just to execute a curl command is wasteful. An external service makes the HTTP request directly.
  • Cross-platform scheduling. If your architecture spans multiple clusters, serverless functions, or external APIs, an external scheduler gives you one place to manage all scheduled tasks.
  • Automatic retries with backoff. Kubernetes retries Pods, but the retry happens at the Pod level. CronJobPro retries at the HTTP level with configurable backoff, which is faster and lighter for API-triggered tasks.

In practice, many teams use both: Kubernetes CronJobs for heavy batch processing (data pipelines, ML training, backups that need cluster resources) and an external scheduler like CronJobPro for HTTP-triggered tasks (webhooks, API calls, health checks, frequent polling).

Summary

  • Kubernetes CronJobs use the standard 5-field cron syntax and create Jobs on schedule.
  • Always set concurrencyPolicy, startingDeadlineSeconds, and activeDeadlineSeconds in production.
  • Monitor with kube-state-metrics and set up alerts for missed schedules.
  • Use the timeZone field (K8s 1.27+) instead of manual UTC conversion.
  • For HTTP-triggered tasks, consider an external scheduler for better monitoring and simpler setup.

Related Articles

Schedule HTTP jobs without Kubernetes complexity

CronJobPro handles scheduling, retries, and monitoring for any HTTP endpoint. Free for up to 5 jobs.