How to Schedule a Recurring Task With a Kubernetes CronJob
A recurring cluster task - pruning old rows, nightly backups, report rollups - belongs in a Kubernetes CronJob, not a hand-run script. Set a schedule in standard cron syntax (for example */5 * * * *), give it a jobTemplate that runs a container and exits, and add successfulJobsHistoryLimit: 3 so completed Jobs do not accumulate. Here is the full manifest and how to apply it.
What a CronJob actually does
A Kubernetes CronJob is the in-cluster equivalent of a crontab entry. At each
time its schedule fires, the CronJob controller creates a Job, and that Job
runs a pod that executes your task and exits. The three parts you always set are
the schedule (when it runs), the jobTemplate (what it runs), and the history
limits (how many finished Jobs to keep around).
The schedule field uses standard five-field cron syntax:
┌───────── minute (0-59)
│ ┌─────── hour (0-23)
│ │ ┌───── day of month (1-31)
│ │ │ ┌─── month (1-12)
│ │ │ │ ┌─ day of week (0-6, Sun=0)
│ │ │ │ │
*/5 * * * * → every 5 minutes
0 * * * * → top of every hour
0 3 * * * → 03:00 every day (a nightly backup)
Write the CronJob manifest
Create cronjob.yaml. This one runs a cleanup task every five minutes and keeps
only the last three successful Jobs:
apiVersion: batch/v1
kind: CronJob
metadata:
name: cleanup
spec:
schedule: "*/5 * * * *"
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: cleanup
image: busybox:1.36
command: ["sh", "-c", "echo pruning old rows; sleep 1"]
A few details that matter:
apiVersion: batch/v1is the stable API since Kubernetes 1.21 - do not use the oldbatch/v1beta1.restartPolicy: Never(orOnFailure) is required on a Job pod template. The defaultAlwaysis invalid for a Job and the manifest will be rejected.successfulJobsHistoryLimitbounds how many completed Jobs (and their pods) stick around. Without it, finished pods accumulate in the namespace until they clutterkubectl get podsand consume the namespace quota.
Apply and verify
kubectl apply -f cronjob.yaml
kubectl get cronjob cleanup
kubectl get cronjob shows the schedule, the suspend state, and the last-run
columns:
NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE
cleanup */5 * * * * False 0 <none> 8s
LAST SCHEDULE stays <none> until the schedule first fires. You do not have to
wait five minutes to test the task - trigger a one-off Job from the CronJob's own
pod spec:
kubectl create job --from=cronjob/cleanup test-run
kubectl logs job/test-run
That runs the exact same container immediately, so you can confirm it does the right thing and exits cleanly before trusting the schedule.
Stop overlapping runs
If a run can take longer than the interval between runs, add a concurrencyPolicy
so a slow Job does not overlap the next scheduled one:
spec:
schedule: "*/5 * * * *"
concurrencyPolicy: Forbid
Forbid skips the new run if the previous one is still going; Replace kills the
running Job and starts fresh. The default, Allow, lets them overlap - which is
usually not what you want for a cleanup or backup task. Getting the schedule right
and choosing the concurrency policy are the two things people most often get wrong.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Writing a CronJob manifest with a cron schedule, jobTemplate, and history limits
- Triggering a one-off Job from a CronJob with kubectl create job --from=cronjob/...
- Choosing a concurrencyPolicy so a slow run does not overlap the next schedule
FAQ
How do I schedule a recurring task in Kubernetes?
Create a CronJob with a schedule in standard cron syntax and a jobTemplate that runs your container. For example schedule "*/5 * * * *" runs every five minutes; apply it with kubectl apply -f cronjob.yaml and confirm with kubectl get cronjob.
What cron schedule format does a Kubernetes CronJob use?
It uses standard five-field cron syntax: minute, hour, day-of-month, month, day-of-week. So "0 3 * * *" runs at 03:00 daily and "*/5 * * * *" runs every five minutes. The controller schedules a Job each time the expression matches.
How do I stop finished Kubernetes Jobs from piling up?
Set successfulJobsHistoryLimit and failedJobsHistoryLimit on the CronJob spec. They cap how many completed and failed Jobs (and their pods) are retained, so old runs are garbage-collected instead of cluttering the namespace and consuming its quota.
How do I manually trigger a Kubernetes CronJob to test it?
Run kubectl create job --from=cronjob/<name> <job-name>. This creates a one-off Job using the CronJob's exact pod spec, so you can run the task immediately and read its logs with kubectl logs job/<name> without waiting for the schedule to fire.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →