CronJob in Kubernetes
A beginner-friendly guide to running tasks on a timetable inside a cluster: what a CronJob is, how it works on the inside, how to read schedules, how to stop runs from colliding, and how to keep it safe and visible. Every new word is explained the moment it appears.
Think about the small chores that keep a home or an office running. The rubbish truck comes every Tuesday. The office cleaner arrives at seven each evening. The bank statement is printed on the first of every month. Nobody stands guard waiting for these moments. Someone wrote the timetable once, and the work simply happens on time, week after week, whether or not anybody is watching.
Software needs exactly the same kind of timetable. A database must be backed up every night. Old files must be cleared every weekend. A sales report must be emailed every morning at nine. In Kubernetes, the object that carries out such repeating work is called a CronJob. This guide explains it slowly and completely, starting from zero, with everyday comparisons, clear diagrams, and the traps that catch even experienced engineers.
AThe Short Answer: What Is a CronJob?
If you only remember one paragraph, make it the next one.
A CronJob is a Kubernetes object that creates a new Job again and again on a repeating schedule that you write in a short text pattern. A Job is a one-time task that runs a container until it finishes successfully. So a CronJob is a timetable, and each tick of the timetable produces one fresh Job, which in turn starts a pod to do the actual work. The word cron comes from the classic Unix scheduling tool that has existed since the 1970s, and its name comes from the Greek word for time.
A CronJob is a standing order at a bakery. You say once, “deliver twenty loaves every morning at six”. After that, you never phone again. Each morning the bakery prepares a fresh delivery, and once the loaves are handed over, that day’s order is finished. The standing order is the CronJob. Each morning’s delivery is a Job.
The three words you must know
Pod
The smallest unit Kubernetes runs. It wraps one or more containers, which are sealed packages holding an application and everything it needs.
Job
A task that runs to completion, once. It creates pods, watches them, retries if they fail, and stops when the work has succeeded.
CronJob
A timetable that creates a new Job every time its schedule says so. It is the recurring version of a Job.
Where CronJobs fit among Kubernetes workloads
Kubernetes has different objects for different kinds of work. A Deployment runs programs that should stay alive forever, such as a website. A Job runs a task once until it succeeds. A CronJob runs a task again and again on a timetable. A DaemonSet runs one helper on every machine. Choosing the right one is the first design decision, and using a Deployment for a task that should run at night is a classic beginner mistake.
A short history
The idea of a scheduled task is very old. Unix computers have had a cron program since the 1970s, and administrators used a single file on a single server to list their timetable. That was simple, but if the server died, the timetable died with it, and nobody could easily see what ran where. Kubernetes arrived with a better idea: keep the timetable as an object in the cluster, run each task in a fresh, isolated pod on any healthy machine, and let the cluster handle the retries. The feature first appeared around 2016 under the name ScheduledJob, was renamed CronJob soon afterwards, and became fully stable in Kubernetes 1.21.
Long-running program means Deployment. One-time task means Job. Repeating task on a clock means CronJob. Keep this triangle in your head and most workload questions answer themselves.
BBuilding Blocks: From Container to CronJob
A CronJob is the top of a small ladder. Understanding each rung makes the top one obvious.
A Kubernetes cluster is a group of computers working as one. Some computers make decisions and form the control plane, while others run applications and are called worker nodes. Whatever you run ends up as a container inside a pod on a worker node. The CronJob sits at the top of a chain that reaches down to that container.
flowchart TB
CJ["CronJob: the timetable"] -->|"creates at each scheduled time"| J1["Job for Monday 02:00"]
CJ -->|"creates at each scheduled time"| J2["Job for Tuesday 02:00"]
CJ -->|"creates at each scheduled time"| J3["Job for Wednesday 02:00"]
J3 -->|"creates"| P1["Pod attempt 1"]
P1 -->|"runs"| C1["Container does the work"]
J3 -.->|"if attempt 1 fails, creates"| P2["Pod attempt 2"]
P2 --> C2["Container does the work"]
What a Job promises
The Job is where most of the useful behaviour lives, so let us look at it closely. A Job says: “run this pod until it has finished successfully, and try again if it fails”. You can set how many successful completions are needed, how many pods may run at the same time, and how many retries are allowed before giving up. That retry limit is called backoffLimit and its default is six. Between retries, Kubernetes waits longer each time, starting near ten seconds and doubling until it reaches a ceiling of about six minutes. This is called exponential backoff, and it prevents a broken task from hammering the cluster.
Think of a courier delivering a parcel. If nobody answers the door, the courier tries again later, waits longer after each failed visit, and finally returns the parcel to the depot after too many tries. The Job is the courier, the retry limit is the number of visits allowed, and the growing wait is the backoff.
Two special rules for job pods
Every pod created by a Job must have a restart policy of either Never or OnFailure. With Never, a failed pod stays in place as evidence and a new pod is created for the retry. With OnFailure, the failed container is restarted inside the same pod. The usual Always policy, which suits websites, is not allowed, because a task that is supposed to finish would then restart forever.
Splitting big work into many pods
Two settings shape how much work a single Job does. completions says how many pods must finish successfully before the Job counts as done. parallelism says how many of those pods may run at the same moment. If you set completions to ten and parallelism to two, Kubernetes runs pods two at a time until ten have succeeded. This is useful for chopping a big task into slices, such as processing ten batches of customer records.
An indexed Job goes one step further by giving every pod a number, from zero upwards, so each pod knows which slice of the work belongs to it and does not need any coordination. Most CronJobs keep the defaults of one and one, but knowing these settings lets you grow a nightly task from a single pod into a small team without changing how it is scheduled.
The name of a CronJob can be at most 52 characters long. Kubernetes adds a suffix to build each Job’s name, and Job names have their own length limit, so a longer CronJob name fails validation.
CReading and Writing the Schedule
The schedule looks like a puzzle at first. It is really five small boxes read from left to right.
The five fields
A schedule is five values separated by spaces. Reading from the left, they are the minute, the hour, the day of the month, the month, and the day of the week. A star means “every possible value”. A slash means “every so many”. A comma builds a list. A dash builds a range.
| Position | Meaning | Allowed values |
|---|---|---|
| 1 | Minute | 0 to 59 |
| 2 | Hour | 0 to 23 |
| 3 | Day of the month | 1 to 31 |
| 4 | Month | 1 to 12, or short names such as JAN |
| 5 | Day of the week | 0 to 6 with Sunday as 0, or short names such as MON |
Examples in plain words
| Schedule pattern | When it runs |
|---|---|
| * * * * * | Every minute, all day, every day |
| */5 * * * * | Every five minutes |
| 0 * * * * | At the start of every hour |
| 30 2 * * * | Every day at 2:30 in the morning |
| 0 9 * * 1-5 | At 9:00 every weekday, Monday to Friday |
| 0 0 1 * * | At midnight on the first day of every month |
| 15 23 * * 0 | At 11:15 at night every Sunday |
| 0 8,12,18 * * * | At 8:00, 12:00 and 18:00 every day |
Kubernetes also accepts handy shortcuts. The word @hourly means the start of every hour, @daily means midnight each day, @weekly means midnight on Sunday, @monthly means midnight on the first of the month, and @yearly means midnight on the first of January.
Common schedule mistakes
The first trap is the meaning of a star. Writing a star in the minute field with a fixed hour, such as a star followed by 3, does not mean “once at three o’clock”. It means “every minute during the three o’clock hour”, which is sixty runs. To run once, put a specific minute such as 0 in the first field. The second trap concerns the day fields. In classic cron behaviour, when both the day of the month and the day of the week are restricted, a match on either one is enough to run the job, not both together. So a schedule that names the 15th and also names Fridays will run on every 15th and on every Friday. If you want only Fridays that fall on the 15th, you must check the date inside the job itself.
The third trap is the five-versus-six confusion. Some tools use a sixth field for seconds, but Kubernetes does not. If you copy a schedule from another system, count the fields, and remember that the smallest step is one minute.
A schedule pattern is like a combination lock with five dials. Each dial has its own range, and the lock opens whenever all five dials match the current time. A star on a dial means “any position opens this dial”.
Time zones: the quiet source of surprises
By default, the schedule is read in the time zone of the control plane program that manages CronJobs, which is usually Coordinated Universal Time, or UTC, on cloud clusters. So “0 9 * * *” might mean 9:00 in London in winter but 14:30 in India. Since Kubernetes 1.27, you can state a timeZone field with a proper name from the international time zone database, such as Asia/Kolkata or America/New_York, and the schedule is then read in that zone. Do not put time zone words inside the schedule text itself, because recent versions reject that.
In zones that change their clocks, some local times happen twice in autumn and some do not exist at all in spring. A job planned for 2:30 in the morning could run twice or be skipped on those days. For important work, choose UTC or pick an hour that is never affected.
Everyone loves midnight and the top of the hour, so many jobs collide there and strain the cluster. Choose a slightly odd time, such as 2:17, to spread the load.
DInternal Working: The CronJob Controller
Who watches the clock? A small loop inside the control plane, doing a very simple job again and again.
Inside the control plane there is a program called the controller manager, which bundles many small watchers named controllers. One of them is the CronJob controller. Like every Kubernetes controller, it follows a loop: look at the desired state, look at the real state, and fix any gap. Here the desired state is “a Job should exist for every scheduled time that has arrived”, and the real state is the list of Jobs that actually exist.
What happens on every check
Read the CronJob
The controller reads the schedule, the time zone, the last time it created a Job, and the settings for concurrency and deadlines.
Work out the missed times
It calculates every scheduled moment between the last recorded run and now. Usually there is exactly one, or none.
Check the rules
It asks: is the CronJob suspended, is it too late for this run, and is a previous Job still running while the policy forbids overlap?
Create the Job
If everything is fine, it copies the Job template from the CronJob and creates a new Job. The name is the CronJob name plus the scheduled time counted in minutes since 1970, which makes it unique.
Tidy up and wait
It updates the status with the last schedule time, deletes very old Jobs beyond the history limits, and sleeps until the next event or the next scheduled moment.
The CronJob controller is a hotel receptionist who keeps a list of wake-up calls. Each minute the receptionist glances at the list and the clock. If a room’s time has arrived and the guest is not already awake, the receptionist places the call. After the call, the receptionist ticks the room off, and only goes back to it at the next scheduled time.
Good to know about the design
The controller does not run your task itself. It only creates Job records through the API server. From there, the Job controller creates pods, the scheduler chooses a machine for each pod, and the kubelet on that machine starts the containers. This division of labour means each component is simple. It also means that if the control plane briefly stops, running pods keep going, and when it returns, the controller catches up by reading the recorded last schedule time.
What the controller remembers
Every CronJob has a status section that the controller keeps up to date. It contains the time of the last scheduled run, the time of the last successful run in newer versions, and a list of the Jobs that are active at this moment. These few facts are all the controller needs to rebuild its picture after any restart. They also give you a quick way to answer the questions “when did it last run?” and “is anything running right now?” without reading logs. Monitoring tools read the same fields, as you will see in a later chapter.
The Kubernetes documentation is honest that a CronJob creates a Job roughly once per scheduled time. In rare circumstances, two Jobs might be created, or none. For this reason, every task you schedule should be safe to run twice. Engineers call such tasks idempotent, meaning that repeating them causes no extra harm.
EConcurrency Policy: What If the Last Run Is Still Going?
Imagine a nightly backup that normally takes twenty minutes, but one night takes ninety. What should happen when the next schedule time arrives?
Each CronJob has a setting called concurrencyPolicy that answers this question. There are three choices, and picking the right one protects your data.
flowchart TD
T["Scheduled time arrives"] --> Q{"Is the previous Job still running?"}
Q -->|"No"| N["Create the new Job"]
Q -->|"Yes"| P{"concurrencyPolicy"}
P -->|"Allow"| A["Create the new Job too, both run together"]
P -->|"Forbid"| F["Skip this run and count it as missed"]
P -->|"Replace"| R["Stop the old Job, then create the new Job"]
| Policy | Behaviour | Best used when | Danger |
|---|---|---|---|
| Allow (default) | Starts a new Job even if the old one is still running | Runs are fully independent, such as sending separate notifications | Overlapping runs can fight over the same data or overload the cluster |
| Forbid | Skips the new run while the old one continues | Two runs at once would corrupt something, such as a backup or a data migration | Skipped runs leave a gap if the old run is slow or stuck |
| Replace | Cancels the old run and starts the new one | Only the freshest result matters, such as refreshing a cache | Long tasks may never finish if they are always replaced |
Picture a cleaner scheduled to mop the hall every hour. If the last mopping is not finished when the next hour strikes, the manager can say “let a second cleaner start anyway” (Allow), “wait, do not start another, the first one is still busy” (Forbid), or “stop the first one and start fresh” (Replace). Each answer suits a different kind of mess.
Choosing a concurrency policy: a quick guide
Ask three questions about your task. First, does a second copy running at the same time cause harm, such as two writers changing the same records? If yes, avoid Allow. Second, is an old result still useful once a newer one is due? If not, prefer Replace, because the freshest answer is all that counts. Third, is it acceptable to skip a run entirely? If the task must eventually complete every period, Forbid could leave gaps, so pair it with a reasonable deadline and an alert. Writing the answers next to the CronJob description in your repository helps the next engineer understand the choice.
Because the default is Allow, a slow job can slowly pile up copies of itself, each making the others slower. This runaway effect has caused many outages. When in doubt, choose Forbid and set a deadline so that a stuck run is stopped eventually.
FMissed Runs, Deadlines, and Suspending
Clusters are not perfect. Sometimes the moment passes before anything can act. Kubernetes gives you settings to decide how late is too late.
What is a missed run?
A run is missed when its scheduled time has passed but no Job was created. This can happen because the control plane was down, because the CronJob was suspended, or because a Forbid policy skipped it. When the controller returns, it looks at the missed times and decides whether to create a Job now or to let them go.
startingDeadlineSeconds: the lateness allowance
The setting startingDeadlineSeconds says how many seconds after the scheduled moment a run may still begin. If the controller is late by more than that, it counts the run as failed to start and skips it. If you do not set it, there is no deadline, and the controller will start the latest missed run whenever it finally notices, but only that latest one, not every one in the backlog.
Always catch up
A missed run starts late, whenever the controller can. Good for work like backups where a late run is better than none.
Only if roughly on time
A run that is too late is dropped. Good for work like sending a “good morning” report that is useless in the afternoon.
The limit of one hundred
The controller is careful not to flood the cluster after a long outage. If it finds more than one hundred missed schedules since the last recorded run, it refuses to start the Job and logs an error. This happens mostly with schedules that fire every minute after a long break, or when a deadline setting is very generous. Setting a deadline shortens the window it needs to look back and avoids the problem.
Suspending a CronJob
There is a simple switch named suspend. When set to true, the controller stops creating new Jobs, but Jobs already running are left alone. It is the polite way to pause work during maintenance or while investigating a problem, without deleting the CronJob and losing its settings. Be aware that the runs that pass while suspended are counted as missed, so when you resume, the most recent one may start immediately unless a deadline says it is too late.
Suspending a CronJob is like putting a hold on a newspaper delivery while you go on holiday. The subscription stays, but no papers are delivered. When you return, you may find that the delivery person leaves only the latest copy, not every missed issue.
You can also ask Kubernetes to create a Job from a CronJob’s template on demand, outside the schedule. This is perfect for testing a new CronJob without waiting for the clock, and for re-running a failed nightly job by hand.
GFailures, Retries, and Cleaning Up
Things go wrong at three in the morning. A good CronJob expects that, retries sensibly, and leaves the cluster tidy.
The safety nets for a single run
backoffLimit
How many times the Job retries failed pods before declaring the whole Job failed. The default is six, with growing waits between tries.
activeDeadlineSeconds
The longest a Job may run in total. When time runs out, its pods are stopped. This rescues you from tasks that hang forever.
restartPolicy
Never creates fresh pods for each retry. OnFailure restarts the container inside the same pod.
parallelism and completions
How many pods may work at once and how many successful finishes are needed. Most CronJobs use one of each.
Keeping history under control
Every run leaves behind a Job and its pods. Without cleanup, a job that runs every minute would create more than fourteen hundred Job records a day and slowly clog the control plane database, which is only meant for small records. Kubernetes gives you two kinds of cleanup.
| Setting | Where it lives | What it does | Default |
|---|---|---|---|
| successfulJobsHistoryLimit | CronJob | How many finished successful Jobs to keep for inspection | 3 |
| failedJobsHistoryLimit | CronJob | How many failed Jobs to keep so you can study what went wrong | 1 |
| ttlSecondsAfterFinished | Job template | Automatically deletes a Job and its pods this many seconds after it ends | Not set, so no time-based cleanup |
The history limits keep the most recent records so that you can read the logs of the last few runs. The time-based setting removes each Job on a timer, which is useful when you ship logs elsewhere and do not need the pod to linger. Many teams use both: a small history limit for quick inspection and a time limit as a safety net.
Different kinds of failure
It helps to separate failures into three kinds. A program failure happens when your own code exits with an error, perhaps because a database is unreachable. A pod failure happens when the container is killed from outside, for example because it used more memory than its limit, or because its node was drained for maintenance or ran out of resources. A Job failure happens when the retries run out or the active deadline passes. The Job treats program and pod failures alike by creating a replacement pod, up to the backoff limit. Reading the pod’s final state tells you which kind you have: an out-of-memory kill needs a larger memory limit, while a program error needs a fix in the code or its settings.
Newer versions of Kubernetes also let a Job follow a pod failure policy, which lets you say “if the container exits with this special code, stop retrying at once”, or “if the pod was lost because its node was removed, do not count this against the retry limit”. This saves you from wasting retries on errors that will never fix themselves, such as a missing configuration file.
History limits are like a receipt drawer where you keep only the last three receipts and the last failed one, and throw older ones away. The time-based cleanup is like a shredder that destroys any receipt automatically after a week, whether or not the drawer is full.
When a Job is deleted, its pods and their logs disappear too. If you need to investigate a failure from last Tuesday, make sure logs are shipped to a central place before cleanup removes them. A failed-jobs limit of one means only the newest failure is inspectable in the cluster itself.
HData Flow and Lifecycle: One Run from Tick to Finish
Let us watch one scheduled run travel through the whole system, from the clock striking to the last container exiting.
sequenceDiagram
participant C as CronJob controller
participant A as kube-apiserver
participant J as Job controller
participant S as kube-scheduler
participant K as kubelet on chosen node
C->>C: Clock reaches scheduled time
C->>A: Create Job from the CronJob template
A-->>C: Job saved
J->>A: Watch shows new Job, create Pod
S->>A: Watch shows Pod without a node, bind it
K->>A: Watch shows Pod assigned to me
K->>K: Pull image and start the container
K->>A: Report Pod Succeeded
J->>A: Mark Job Complete
C->>A: Update last schedule time and prune old Jobs
The life of a single scheduled run
Due
The scheduled time arrives, and the CronJob controller decides a Job is needed.
Job created
A Job record appears, copied from the template, with a name based on the scheduled time.
Pod created and placed
The Job controller creates a pod, and the scheduler picks a worker node for it. Pods that do not fit stay Pending until room appears.
Running
The kubelet pulls the image if needed and starts the container. Your program does its work and writes messages to standard output.
Finished
The container exits. A zero exit code means success, and anything else means failure and triggers a retry within the limits.
Recorded and cleaned
The Job is marked Complete or Failed. The CronJob status records the last successful time, and old Jobs beyond the limits are removed.
What you see when you list CronJobs
A listing shows a short table with the schedule, whether the job is suspended, how many Jobs are active right now, when it last ran, and how old the CronJob is. The active count is the quickest way to spot a stuck run: if it stays at one long after the expected end time, something is hanging. The last-schedule column tells you whether the timetable is still ticking at all.
Watching a run as it happens
When you test a new CronJob, it is instructive to watch the objects appear in real time. A Job record shows up first, then a pod in the Pending state while the scheduler finds it a home, then the pod moves to Running as the container starts, and finally to Completed when the exit code is zero. If the pod sits in Pending, look at the scheduler’s message. If it shows an image pull problem, check the image name and registry access. If it enters an error state and restarts, read the container’s logs first. Seeing this sequence once with your own eyes makes every later problem easier to place.
Kubernetes judges success only by the container’s exit code. A script that prints “error” but exits with zero will be recorded as successful. Make sure your program exits with a non-zero code when the work fails, or failures will pass silently.
IAdvantages, Disadvantages, and Trade-offs
CronJobs are simple and popular, but they are not the answer to every scheduling problem. Knowing their limits saves pain.
Advantages
- Uses the same tools, images, and permissions as the rest of your cluster
- Each run is isolated in its own pod, so a crash cannot harm other runs
- Built-in retries, deadlines, and history keep runs tidy
- Pods are placed on any machine with room, so you need no special server for cron
- Configuration lives in files that can be reviewed and stored in version control
Disadvantages
- Smallest gap between runs is one minute
- Delivery is roughly once, not exactly once, so tasks must be idempotent
- No built-in way to say “run B only after A succeeds”
- Time zone and daylight saving details can surprise you
- Logs and history disappear when Jobs are cleaned up
CronJob compared with other ways to run scheduled work
| Approach | Strength | Weakness | Choose it when |
|---|---|---|---|
| Kubernetes CronJob | Simple, native, isolated runs | No workflow dependencies, one-minute minimum | Independent recurring tasks inside a cluster |
| A Deployment with a sleep loop | Can run every few seconds | Always consumes resources, needs manual care after crashes | Almost never for real schedules, use a proper service instead |
| Workflow engines such as Argo Workflows or Apache Airflow | Dependencies, retries per step, visual history | More to install and learn | Multi-step pipelines where step order matters |
| Cloud schedulers such as Amazon EventBridge Scheduler | Managed, works even without a cluster | Tied to one cloud provider | Triggering cloud functions or starting tasks outside Kubernetes |
| An old cron on a single server | Familiar | The server is a single point of failure and is invisible to the platform team | Only on small, non-cluster setups |
Moving from an old server cron to a CronJob
Many teams begin their journey by copying a line from a crontab on an old server into a CronJob. A safe way to do this has four steps. First, package the script and its tools into a container image, so it no longer depends on whatever happened to be installed on that server. Second, move passwords out of the script and into Secrets. Third, run the CronJob in a test namespace next to the old cron for a week, and compare results. Fourth, switch off the old cron only when the new one has proved itself and alerts are in place. This gradual path removes the fear of change, and it often reveals hidden assumptions, such as a script that quietly relied on a file that only existed on the old machine.
A CronJob is a reliable kitchen timer. It is excellent at ringing when the time comes. But it cannot organise a whole banquet in which the soup must finish before the roast begins. For that you need a head chef, which is what workflow engines provide.
Choose a CronJob for small, independent, recurring tasks. When you find yourself chaining CronJobs by guessing that the first one “should be finished by now”, it is time to move to a workflow engine.
JResources, Scalability, and Reliability of Scheduled Work
A task that runs for a few minutes a day still needs room on the cluster when it starts. Planning that room is part of the design.
Give every task honest resource numbers
Every container can declare a request, the amount of processor and memory it needs reserved, and a limit, the ceiling it may not cross. The scheduler places the pod using the request. If a nightly job asks for too much, the pod may sit Pending because no machine has enough room. If it asks for too little, it may be slowed down or killed when it runs out of memory. Measure a few real runs and set numbers close to what you saw.
Passing settings and data to a job
A scheduled task rarely works alone. It needs settings, credentials, and a place to read and write data. Settings that are not secret go in a ConfigMap, and secret values go in a Secret. Both can be handed to the container as environment variables or as files. Data should live outside the pod, in object storage such as Amazon S3, in a database, or on a shared volume, because the pod’s own disk disappears when the run ends.
Be careful with shared volumes. Many disk types can be attached to only one node at a time. If two runs overlap on different nodes and both need the same disk, one will wait. Object storage avoids this problem and is the usual choice for backups and reports. It also helps to pass the date or time window to the job as an argument, so the task knows exactly which slice of data to handle, rather than guessing from the clock at the moment it starts.
The thundering herd
When many CronJobs are set for the same minute, especially midnight, they all start at once. Hundreds of pods appear together, all pulling images, all asking for room. This sudden crowd is called a thundering herd. It can delay the jobs, slow other applications, and trigger the node autoscaler to add machines just to handle a few minutes of work. The cure is simple: spread schedules over different minutes, and where possible keep batch work on its own group of nodes.
Stagger start times
Use odd minutes like 2:07 and 2:23 instead of everyone choosing the hour mark.
Dedicated batch nodes
Mark a group of nodes with a taint, and let only batch pods tolerate it, so nightly work cannot starve web traffic.
Use cheaper capacity
Interruptible jobs can run on discounted spare machines, provided they retry safely when a machine is taken away.
Set quotas
A ResourceQuota limits how much a team’s namespace can use, so a runaway job cannot eat the entire cluster.
What happens if the control plane is down?
Running Jobs and pods keep going, because the worker nodes already know what to do. But no new Jobs are created while the CronJob controller is unavailable. When the control plane returns, the controller compares the clock with the last recorded run and starts the latest missed one, subject to the deadline you set. This is why important jobs need both a sensible startingDeadlineSeconds and an alert that tells you when a run did not happen.
| Event | Effect on scheduled work | Protection |
|---|---|---|
| Worker node fails during a run | The pod is lost, and the Job creates a replacement up to the retry limit | Make the task safe to restart |
| Control plane briefly unavailable | A run may start late or be skipped | Set a deadline and alert on missing runs |
| Image cannot be downloaded | The pod waits and retries, and the run is delayed | Use a reliable registry and pinned image versions |
| No room on any node | The pod stays Pending | Set honest requests and use a node autoscaler |
| Task hangs forever | Later runs may be blocked by a Forbid policy | Set an active deadline on the Job |
The worst CronJob failure is the one nobody notices, such as a backup that quietly stopped running months ago. Always add a way to detect “it did not run”, not only “it ran and failed”.
KSecurity for Scheduled Work
A CronJob is a machine that runs code on its own, with real permissions, while nobody is watching. That deserves care.
A CronJob is like a night security guard who has keys to rooms and works alone. You would choose the guard carefully, give keys only to the rooms needed, and keep a written log of what was done. The same care applies to a scheduled task.
Key protections
Dedicated service account
Give each CronJob its own identity with only the permissions it needs. A report job should never share the identity of an administrator.
Control who can create CronJobs
Anyone who may create a CronJob can make the cluster run code on a schedule. Limit that right with role-based access control.
Never bake passwords into images
Pass credentials at run time from Secrets or from an external vault such as AWS Secrets Manager, and rotate them regularly.
Run as a normal user
Use a non-root user, a read-only file system where possible, and the restricted Pod Security level, so a hijacked job can do little harm.
Limit what it can reach
Network policies can allow a backup job to contact only the database and the storage bucket, and nothing else.
Trust your image
Use small, scanned images with pinned version tags, taken from a registry you control. A scheduled run repeats any bad image many times.
Why repetition raises the stakes
A vulnerable web page is attacked when someone finds it. A vulnerable CronJob is executed on schedule whether or not anyone is looking. If its image is compromised, or its script downloads code from an unknown place at each run, the harm repeats itself every night. Attackers have also abused CronJobs in hijacked clusters as a place to hide, because a small job that starts every hour looks harmless. Auditing the list of CronJobs and their images from time to time is a cheap and valuable habit.
Sharing a cluster between teams
In a shared cluster, each team usually gets its own namespace, which is a named area that keeps objects separate, like floors in an office block. Scheduled work belongs in the namespace of the team that owns it, so that permissions, quotas, and costs stay clear. Set a ResourceQuota on each namespace to cap the total processor, memory, and number of pods it may use, and a LimitRange to give sensible default requests to containers that forget to declare them. Together they stop one team’s forgotten job from starving everybody else. It also helps to label every CronJob with the owner’s name and a contact channel, so that whoever notices a problem at night knows whom to wake.
Backup jobs often hold the keys to every database and storage bucket. Treat them as your most sensitive workloads, give them narrow permissions, and keep their output in a bucket that ordinary application roles cannot delete.
LMonitoring, Logging, and Metrics
Scheduled jobs run in the dark. Good monitoring turns on the lights only when something is wrong.
Think of a night watchman who must ring a bell every hour. If you only listen for alarms, you will miss the night when the watchman fell asleep and the bell never rang. You need to listen for the bell itself, and raise the alarm when it is missing. Monitoring CronJobs works the same way.
Two kinds of failure to catch
The run started and failed
Pods crashed, retries ran out, or the Job ended in a failed state. This is easy to see in metrics and events.
The run never happened
The schedule was skipped, suspended by mistake, or blocked. Nothing failed, because nothing started. This needs an alert on absence.
What to watch
A helper called kube-state-metrics turns the state of Kubernetes objects into numbers that Prometheus, the popular monitoring system, can collect. It publishes the time of the next scheduled run, the time of the last run, the time of the last success, and how many Jobs are currently active. It also reports how many Jobs have succeeded or failed. Grafana can draw these numbers, and Alertmanager can send alerts.
| Signal | Why it matters |
|---|---|
| Time since last successful run | The best single alert: if it grows beyond one expected period, the job is broken or silent |
| Failed Jobs count | Shows runs that exhausted their retries |
| Active Jobs count | A value stuck above zero for a long time means a hung run |
| Time from schedule to pod start | Growing delay points to capacity shortage or a slow image download |
| Run duration | A slow upward trend warns you before a job starts overlapping its own next run |
| Suspended flag | Reveals CronJobs paused and forgotten during maintenance |
flowchart LR
CJ["CronJob and Job objects"] --> KSM["kube-state-metrics"]
KSM --> PR["Prometheus"]
POD["Job pods write logs to standard output"] --> LA["Log agent on each node"]
LA --> LS["Central log store"]
PR --> GF["Grafana dashboards"]
PR --> AM["Alertmanager"]
AM --> ON["On-call engineer"]
JOBEND["Job pings a heartbeat service on success"] --> DMS["Heartbeat monitor that expects a daily signal"]
DMS --> ON
The dead man’s switch
A powerful trick is to make each successful run send a tiny “I am alive” message to an outside monitoring service at the end of its work. That service expects the message every day. If a day passes without one, it raises an alarm. This approach catches every failure, even those where Kubernetes itself is broken, because it depends on evidence of success rather than on the absence of failure.
Reading the cluster’s own summary
Before setting up any dashboard, get comfortable with the built-in views. Listing the CronJobs in a namespace shows the schedule, the suspend flag, the count of active Jobs, the time since the last schedule, and the age. Listing the Jobs shows how many completions each achieved and how long it took. Describing one Job shows its events in plain sentences, such as pods created, pods failed, and the reason the Job ended. These three views answer ninety percent of the questions you will have at three in the morning, and knowing them well makes you faster than any tool.
Logging
Programs in a job should print messages to standard output, and a log agent should copy them to a central store before the pod is cleaned up. Include the run’s date and key numbers in the messages, such as “copied 12,340 records”. When a report looks wrong next week, those lines are the evidence you will want.
Set your “no successful run” alert to about one and a half times the schedule period. A daily job should alert after thirty-six hours without success, which tolerates a small delay but catches a real miss.
MDesign Patterns and Anti-Patterns
Reliable scheduled tasks share a few habits. Unreliable ones share a different set of shortcuts.
Patterns that work well
Idempotent tasks
Design each run so repeating it changes nothing new. For example, write to a file named by date and overwrite instead of appending.
Process by time window
Tell each run exactly which slice of time to process, such as “yesterday”, rather than “everything new”. A late or repeated run then handles the same slice.
Checkpointing
Save progress as you go, so a run restarted after failure continues instead of starting over.
Small and focused jobs
One job, one purpose. Several small jobs are easier to retry and monitor than one giant script.
Trigger, then hand off
Let the CronJob start a lightweight call to a service that does the heavy work, keeping the schedule and the logic separate.
Lock for exclusivity
When two runs must never overlap and Forbid is not enough, take a lock in a shared store and release it at the end.
An idempotency example
Suppose a job sends a daily summary email to customers. A careless design says “send an email to everyone” every time it runs. If Kubernetes retries after a partial failure, half the customers receive two emails. A careful design keeps a small table listing which customers have already received today’s summary. The job first asks “who has not yet received today’s email?”, sends only to those, and marks each one as done immediately after sending. Now the job can be retried, duplicated, or run by hand, and the result is always the same: each customer receives exactly one email. This small habit is the single most valuable pattern in scheduled work.
An idempotent task is like a light switch labelled “turn on”. Pressing it twice leaves the light on, no harm done. A non-idempotent task is like a button labelled “add one spoon of sugar”. Pressing it twice ruins the tea.
Anti-patterns to avoid
What it is
Writing tasks that are not safe to repeat, such as sending payment emails or adding rows without checking whether they already exist.
Why it hurts
Kubernetes only promises roughly-once delivery. Retries, duplicate Jobs, or a manual re-run can double the effect and cost you money or trust.
Better approach
Record what has been done, check before acting, and use unique keys so a repeated run has nothing new to change.
What it is
Leaving the default Allow policy on a job that can take longer than its schedule gap, with no deadline.
Why it hurts
Slow runs pile on top of each other. Each makes the others slower, memory fills up, and eventually the whole cluster feels the strain.
Better approach
Choose Forbid or Replace deliberately, and add an active deadline so a stuck run is always stopped.
What it is
Faking a faster schedule by running a loop with sleep commands inside a CronJob’s container, for example to act every ten seconds.
Why it hurts
You have built a fragile long-running service in disguise, which overlaps with the next run, hides failures, and defeats the cleanup logic.
Better approach
If you need sub-minute timing, use a proper Deployment that runs the loop deliberately, or an event-driven design.
What it is
Chaining tasks by scheduling the second job “thirty minutes after” the first and hoping the first has finished.
Why it hurts
The day the first job takes forty minutes, the second one works on incomplete data and produces wrong results without any error.
Better approach
Use a workflow engine that starts the second step only after the first succeeds, or combine both steps into one job.
NBest Practices and Common Mistakes
These habits come from many nights of failed backups and silent reports. Copy them early and skip the pain.
Habits of a reliable CronJob
Make the task safe to repeat
Assume it will sometimes run twice or be retried. Design every step so repetition does no harm.
Choose the concurrency policy on purpose
Pick Forbid for anything that must not overlap, and write down why you chose it.
Set every safety limit
Give the Job an active deadline, a sensible retry count, and a start deadline on the CronJob, so no run can hang or be forgotten forever.
Declare requests and limits
Tell the scheduler how much room the task needs, based on measured runs.
State the time zone
Use the timeZone field and prefer UTC for critical tasks, to avoid daylight saving surprises.
Keep history small, logs central
Limit stored Jobs, set a time-based cleanup, and ship logs to a store where you can search them later.
Alert on the absence of success
Watch the time since the last successful run and consider a heartbeat check for the most important jobs.
Pin the image version
Use an exact image tag or digest, so a surprise change upstream cannot alter tomorrow’s run.
Common mistakes and fixes
| Mistake | What happens | Fix |
|---|---|---|
| Forgetting the time zone | The job runs at an hour nobody expected | Set the timeZone field and document it |
| Everyone schedules at midnight | A thundering herd slows the cluster | Stagger start times across the hour |
| Restart policy left at Always | The CronJob is rejected as invalid | Use Never or OnFailure |
| Script exits with zero after an error | Failures are recorded as success | Return a non-zero exit code on failure |
| No active deadline | A hung run blocks later runs forever | Set an active deadline larger than the normal duration |
| Unlimited history | Old Jobs and pods clutter the cluster | Set history limits and a time-based cleanup |
| Trusting the “latest” image tag | A changed image breaks tonight’s run | Pin exact versions |
| Suspended and forgotten | A paused backup goes unnoticed for weeks | Alert on suspended CronJobs and on missing successes |
| Name longer than 52 characters | The CronJob fails validation | Shorten the name |
| Testing only by waiting for the clock | Bugs appear at three in the morning | Create a Job manually from the template and test it first |
While developing, run the job every few minutes in a test namespace and watch several cycles, including one forced failure. Then switch to the real schedule. You will find the surprises while it is cheap.
OReal-World Examples
CronJobs are quiet workers. Here are the tasks teams most often hand to them.
Nightly database backup
A shopping company copies its database to cloud storage every night at a quiet hour, using a job with a dedicated service account that can write to one bucket only. The policy is Forbid, because two backups at once would slow the database, and an alert fires if a whole day passes without a success. Restore tests are run monthly to prove the backups really work.
Cluster housekeeping
Platform teams commonly use CronJobs to snapshot etcd, the control plane database, to remote storage, to remove finished Jobs from old namespaces, and to delete leftover test resources. These tasks protect the cluster itself, so they are the last ones anyone wants to fail silently.
Morning business reports
A finance team generates a sales summary at 6:30 in its local time zone and emails it before offices open. The job uses the timeZone field so the report arrives at the right hour even when the cloud runs in UTC. A short start deadline means a report that would arrive after lunch is skipped instead of confusing people.
Data clean-up and retention
A media site deletes temporary uploads older than seven days and archives old logs every Sunday night. Because the job asks for “everything older than seven days”, running it twice does no harm, which makes it a perfect idempotent task.
Cache warming and data sync
An online store refreshes popular product listings into its cache every fifteen minutes and syncs stock levels from a supplier every hour. The concurrency policy is Replace for the cache refresh, since only the freshest result matters, and Forbid for the supplier sync, to avoid conflicting writes.
Certificate and credential reminders
A security team runs a weekly job that checks how many days remain on important certificates and posts a warning to a chat channel if any expire within thirty days. It costs almost nothing to run and prevents embarrassing outages caused by forgotten renewals.
Choosing between a CronJob and a cloud scheduler
A team on Amazon EKS keeps tasks that need cluster data in CronJobs, and uses Amazon EventBridge Scheduler for tasks that trigger cloud functions outside the cluster. Both tools coexist happily, and the choice depends on where the work actually runs.
A night in the life of a backup job
To make the pieces concrete, imagine a nightly backup set for 02:17 UTC with Forbid, a start deadline of ten minutes, an active deadline of one hour, and a backoff limit of three. At 02:17 the controller creates a Job. The scheduler places its pod on a batch node, and the container starts copying data to storage. At 02:24 the database briefly refuses connections, the program exits with an error, and the Job creates a second pod after a short wait. That attempt succeeds at 02:41, the pod ends with exit code zero, and the Job is marked Complete. The CronJob records the success time, and the heartbeat message tells the outside monitor that tonight’s backup is safe.
Now imagine the control plane was down from 02:10 to 02:35. When it returns, the controller sees that 02:17 was missed by eighteen minutes. That is beyond the ten-minute deadline, so it skips the run, and the missing-success alert fires at the next check, letting a human start the backup by hand. Every setting in this story played a part, and each one was chosen in advance for a reason.
Every good example shares the same traits: a small clear task, safe repetition, honest limits, and an alert for the day it does not run. The scheduling itself is the easy part.
PFrequently Asked Questions
Short, plain answers to the questions beginners ask most.
QSummary and Key Takeaways
Back to the bakery standing order for one last look.
A CronJob is a standing order for your cluster. You describe the task once, write the timetable as five small fields, and Kubernetes delivers a fresh Job at every tick, complete with its own pod, its own retries, and its own record. It keeps its promise most of the time, and your job is to make sure that the rare times it does not are harmless and visible. Choose the concurrency policy on purpose, set deadlines, state the time zone, keep tasks safe to repeat, and watch for the run that never came. With those habits, the humble CronJob becomes one of the most dependable tools in the whole platform.
Key Takeaways
- Timetable for Jobs: A CronJob creates a new Job at each scheduled time, and each Job runs pods until the work succeeds or the retries run out.
- Five-field schedule: Minute, hour, day of month, month, and weekday, with a one-minute minimum. Use the timeZone field and prefer UTC for critical tasks.
- Roughly once, not exactly once: A run may rarely be duplicated or skipped, so make every task idempotent and safe to repeat.
- Control overlap: Choose Allow, Forbid, or Replace deliberately. Forbid is the safest choice for backups and migrations.
- Set the safety limits: Use startingDeadlineSeconds, an active deadline, a backoff limit, history limits, and a time-based cleanup so nothing hangs or piles up.
- Secure the runner: Give each CronJob a narrow service account, keep secrets out of images, run as a normal user, and control who can create CronJobs.
- Watch for silence: Alert on the time since the last successful run and use heartbeat checks, because a run that never started produces no error.