Schedule recurring tasks with CronJob
Learn to schedule recurring tasks in Kubernetes using CronJob. This lesson covers the core concept, step-by-step setup, hands-on exercise, troubleshooting, and what to study next to automate routine jobs.
Focus: schedule recurring tasks with cronjob
If you have ever manually run a database backup script at 2 AM or restarted a cleanup job every hour, you know that recurring tasks are essential — but they are easy to forget. Kubernetes CronJob solves this pain exactly like a Unix crontab, but inside your cluster, with Pod lifecycle guarantees. Instead of relying on fragile shell scripts on a single VM, you get a fault-tolerant, declarative way to run scheduled work.
The problem this lesson solves
Production clusters accumulate cruft: stale logs, expired database records, old container images, or temporary files left by batch jobs. Manually running cleanup commands is error-prone and does not scale. Worse, a missed backup or a forgotten batch can cause silent data loss or storage exhaustion. Kubernetes CronJob gives you a single, cluster-native mechanism to run recurring tasks on a schedule — without installing external tools or maintaining a trigger machine.
Without CronJob, you would need to run a cron daemon inside a Pod or connect to an external job scheduler. Both approaches introduce drift from the cluster state: if the Pod dies, the cron process restarts from scratch, and external schedulers require network reachability and authentication. CronJob integrates directly with the Kubernetes control plane, so the schedule persists even if the scheduler restarts, and job execution is accounted for by the normal Job lifecycle.
Core concept / mental model
Think of a CronJob as a combination of two familiar tools: a Unix crontab and a Kubernetes Job. The CronJob resource specifies:
- a schedule following POSIX cron syntax
- a Job template that defines what runs and how
Every time the schedule fires, Kubernetes creates a new Job object from that template. The Job then spins up one or more Pods, runs the command, and tracks completion. If the command exits with non-zero status, the Job may retry according to its configured backoff limit.
Pro tip: CronJob does not run the command directly — it creates a Job. That means you can inspect Job history, read logs, and apply the same retry or parallelism semantics you already know from standalone Jobs.
Key Definitions
- Schedule: a cron expression in the format
minute hour day-of-month month day-of-week(e.g.,0 2 * * 0means 2 AM every Sunday). - Job Template: the
spec.jobTemplateinside a CronJob manifest — identical to abatch/v1Job. - Concurrency Policy: how to handle overlapping runs (
Allow,Forbid,Replace). - Starting Deadline: how late a CronJob can start if the scheduler misses a scheduled window.
- Successful Jobs History Limit: how many completed Job objects to retain.
How it works step by step
- Create a CronJob manifest in YAML. You define the
schedulestring and thejobTemplatespec insidespec.jobTemplate.spec. - Apply the manifest to the cluster (
kubectl apply -f cronjob.yaml). - The Kubernetes scheduler controller parses the cron expression and stores the expected next run time.
- When the scheduled time arrives, the controller creates a new Job object with a generated name (based on the CronJob name and timestamp).
- The Job controller creates one or more Pods from the Pod template inside the Job, monitors their exit, and updates the Job status.
- History cleanup is automatic: the CronJob controller deletes Jobs older than
successfulJobsHistoryLimitorfailedJobsHistoryLimit.
Concurrency Control
If a job does not finish before the next scheduled time, the concurrencyPolicy decides the outcome:
Allow(default) — run multiple Jobs in parallel.Forbid— skip the new run if the previous run is still active.Replace— terminate the current running Pod and start a new one.
Timezone Support (Kubernetes 1.27+)
You can set spec.timeZone to a valid IANA timezone (e.g., America/New_York). Without it, the controller evaluates the schedule in UTC.
Hands-on walkthrough
Let's create a CronJob that prints a timestamp and a message every minute, then clean up old jobs.
Step 1: Write a CronJob manifest
Create a file named hello-cronjob.yaml:
apiVersion: batch/v1
kind: CronJob
metadata:
name: hello-cronjob
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox:1.36
command:
- /bin/sh
- -c
- date; echo "Hello from CronJob!"
restartPolicy: OnFailure
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
Step 2: Apply the manifest
kubectl apply -f hello-cronjob.yaml
Expected output: cronjob.batch/hello-cronjob created
Step 3: Watch jobs run
kubectl get cronjob hello-cronjob
kubectl get jobs --watch
After a minute, you should see a Job appear. List the Pods that executed the Job:
kubectl get pods --selector=job-name=$(kubectl get jobs -o jsonpath='{.items[0].metadata.name}')
Step 4: Inspect logs
kubectl logs job/hello-cronjob-<id>
Expected output similar to:
Tue Apr 8 15:01:00 UTC 2025
Hello from CronJob!
Step 5: Clean up old jobs
By default, the CronJob keeps the last 3 successful and 1 failed job. To manually delete all completed jobs:
kubectl delete jobs --field-selector status.successful=1
Pro tip: If you delete a CronJob, any running Jobs are not automatically deleted. To cascade, use
--cascade=foreground.
Full Example: Database Backup CronJob
This CronJob takes a pg_dump of a database every night at 2 AM, stores it in a PersistentVolumeClaim, and keeps the last 7 backups:
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-pg-backup
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: pgbackup
image: postgres:16-alpine
env:
- name: PGHOST
value: postgres-service
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: pg-secret
key: password
command:
- pg_dump
- -U
- myuser
- mydb
- -f
- /backups/db-$(date +%Y%m%d).sql
volumeMounts:
- name: backup-storage
mountPath: /backups
restartPolicy: OnFailure
volumes:
- name: backup-storage
persistentVolumeClaim:
claimName: backup-pvc
successfulJobsHistoryLimit: 7
failedJobsHistoryLimit: 2
Compare options / when to choose what
| Feature | CronJob (Kubernetes native) | External scheduler (e.g., Jenkins, cron inside Pod) |
|---|---|---|
| Declarative (kubectl apply) | Yes | No — relies on config map or persistent state |
| Built-in retry & backoff | Yes (via Job template) | Rolling your own |
| Clustered fault tolerance | Full (scheduler replicated) | Single point of failure unless HA |
| Timezone support | Yes (1.27+) | Yes (via TZ env or timezone setup) |
| Observability | kubectl get cronjobs,jobs,pods,logs |
Depends on the tool |
| Cost | Free (included) | Additional service/VM cost |
When to choose CronJob — you are already in a Kubernetes cluster, want minimal external dependencies, and need standard cron semantics with Kubernetes job guarantees.
When to avoid — if you need complex scheduling logic (e.g., cron expression with non-standard intervals like "every 2 hours on weekdays except holidays") or if you already have a mature external batch system.
Troubleshooting & edge cases
Problem 1: CronJob does not create a Job
- Check the schedule expression:
kubectl describe cronjob <name>showsLastScheduleTime. If it isniland status conditions showFailedCreate, check your Job template validity. - Solution: Verify the cron expression using
crontab.guru. A common mistake is using* * * * *which means every minute, not every hour.
Problem 2: Job runs but Pod exits with error
- Symptoms:
kubectl get jobsshowsCOMPLETIONS: 0/1.kubectl describe job <name>showsConditions: Type: Failed. - Solution: Inspect the Pod logs with
kubectl logs pod/<pod-name>. Then fix the container command or environment and re-apply. For a database backup, common issues are incorrect connection strings or missing secrets.
Problem 3: CronJob accumulates thousands of Jobs
- Cause:
successfulJobsHistoryLimitandfailedJobsHistoryLimitare not set or set too high. - Fix: Always specify limits. For high-frequency jobs, set
successfulJobsHistoryLimit: 1andfailedJobsHistoryLimit: 1. You can manually delete older jobs withkubectl delete jobs --field-selector metadata.creationTimestamp<$(date -d '-7 days' +%Y-%m-%dT%H:%M:%SZ).
Problem 4: Timezone offset causes jobs to run at unexpected times
- Root cause: The CronJob controller evaluates schedules in UTC by default. If you set
schedule: "0 2 * * *"expecting local 2 AM, it runs at 02:00 UTC. - Fix: Either set
spec.timeZone: "America/New_York"(Kubernetes 1.27+) or write your cron expression in UTC.
Pro tip: Always test a new CronJob by setting the schedule to
*/1 * * * *first. Once you confirm it works, adjust to the desired frequency.
What you learned & what's next
You now understand how to schedule recurring tasks with CronJob: from writing a cron expression inside a manifest to debugging failed runs and managing history limits. You can:
- Explain the core concept of CronJob as a Job factory triggered by a schedule.
- Write and apply a CronJob manifest for any batch command (backup, cleanup, health check).
- Control concurrency with
concurrencyPolicy. - Use timezone support for human-friendly scheduling.
- Troubleshoot common issues like missing Jobs, failed Pods, and history explosion.
Next in the track, you will learn about resource limits and requests to ensure your CronJob pods do not starve other workloads. This is an essential step before automating any production batch job.
Ready to take the next step? Move on to "Set resource limits and requests" and learn how to budget CPU and memory for your scheduled jobs.
Practice recap
Create a CronJob that runs every 5 minutes and writes the current date and memory usage into a ConfigMap-backed file inside a temporary volume. After confirming it works, change the schedule to run once per hour and set successfulJobsHistoryLimit: 2.
Common mistakes
- Forgetting to set
restartPolicy: OnFailureinside the Job template — the default Pod restart policy for Jobs isNever, but CronJob templates must useOnFailureto allow retries. - Using a cron expression that fires every second (e.g.,
* * * *every minute is correct,* * * * *is invalid and will be rejected). - Not setting
successfulJobsHistoryLimitandfailedJobsHistoryLimit— old Jobs pile up and consume API server resources. - Assuming the CronJob runs in the cluster's local timezone — the controller evaluates schedules in UTC by default unless
spec.timeZoneis set (Kubernetes 1.27+). - Omitting the
concurrencyPolicy— overlapping jobs can run simultaneously by default, which may cause data races or resource exhaustion.
Variations
- Use
concurrencyPolicy: Forbidto skip a run if the previous one is still active — ideal for backup jobs that must not overlap. - Inject environment variables from Secrets or ConfigMaps to avoid hardcoding credentials inside the Job template.
- Combine with
ttlSecondsAfterFinished(since Kubernetes 1.21) to automatically delete completed Job objects after a set period.
Real-world use cases
- Nightly database backup: Create a CronJob that runs
pg_dumpand uploads the result to cloud storage via S3 CLI. - Log rotation and cleanup: Schedule a CronJob to delete log files older than 30 days from a shared PersistentVolume.
- Certificate renewal automation: Run a CronJob every 60 days to check and renew Let's Encrypt TLS certificates stored in Secrets.
Key takeaways
- A CronJob creates a Job on a schedule — the Job template is identical to a standalone
batch/v1Job manifest. - The schedule uses standard POSIX cron syntax with optional timezone support (Kubernetes 1.27+).
- Set
successfulJobsHistoryLimitandfailedJobsHistoryLimitto avoid accumulating thousands of completed Job objects. - Use
concurrencyPolicy: ForbidorReplacewhen overlapping runs are dangerous or wasteful. - Always test with a short schedule (e.g., every minute) before deploying to production frequency.
- CronJob integrates with all Kubernetes observability tools:
kubectl logs,describe, and events.
Keep learning
Related tutorials, quizzes, and articles for this topic.
More in this track
Quizzes
Articles
- Why Chasing 'Perfect Code' Is Actually Making Your Team Slower and More Fragile
- The Rise of Python Notebooks as Operational Tools: Why Jupyter Is Moving Beyond Data Science Into DevOps and Monitoring
- The Legacy Database Problem: Why Python Teams Are Migrating to SQLite and DuckDB for Production Workloads
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.