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: hostApply 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:
| Macro | Equivalent | Description |
|---|---|---|
| @yearly | 0 0 1 1 * | Once a year, midnight Jan 1 |
| @monthly | 0 0 1 * * | Once a month, midnight 1st |
| @weekly | 0 0 * * 0 | Once a week, midnight Sunday |
| @daily | 0 0 * * * | Once a day at midnight |
| @hourly | 0 * * * * | 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.
| Policy | Behavior | Use When |
|---|---|---|
| Allow | New Job runs alongside existing one (default) | Jobs are idempotent and independent |
| Forbid | New Job is skipped if previous is still running | Database operations, backups, anything with locks |
| Replace | Running Job is terminated, new one starts | Cache 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 hourtimeZone (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:
| Metric | What It Tells You |
|---|---|
| kube_cronjob_status_last_schedule_time | When the CronJob last triggered |
| kube_cronjob_status_active | Number of currently running Jobs |
| kube_job_status_succeeded | Whether a Job completed successfully |
| kube_job_status_failed | Whether 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 FreeWhen 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, andactiveDeadlineSecondsin production. - Monitor with kube-state-metrics and set up alerts for missed schedules.
- Use the
timeZonefield (K8s 1.27+) instead of manual UTC conversion. - For HTTP-triggered tasks, consider an external scheduler for better monitoring and simpler setup.
Related Articles
Run scheduled tasks inside Docker containers with cron, Supercronic, and Docker Compose.
Cron Job Monitoring Best PracticesStrategies for monitoring cron jobs: heartbeat checks, log aggregation, alerting, and dashboards.
Automate Database Backups with CronSet up automated MySQL, PostgreSQL, and MongoDB backups with retention and monitoring.
Cron Job Not Running? Here's How to Fix ItTroubleshoot the most common reasons why cron jobs fail silently or refuse to start.
Schedule HTTP jobs without Kubernetes complexity
CronJobPro handles scheduling, retries, and monitoring for any HTTP endpoint. Free for up to 5 jobs.