Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

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

Sponsored

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 * * 0 means 2 AM every Sunday).
  • Job Template: the spec.jobTemplate inside a CronJob manifest — identical to a batch/v1 Job.
  • 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

  1. Create a CronJob manifest in YAML. You define the schedule string and the jobTemplate spec inside spec.jobTemplate.spec.
  2. Apply the manifest to the cluster (kubectl apply -f cronjob.yaml).
  3. The Kubernetes scheduler controller parses the cron expression and stores the expected next run time.
  4. When the scheduled time arrives, the controller creates a new Job object with a generated name (based on the CronJob name and timestamp).
  5. The Job controller creates one or more Pods from the Pod template inside the Job, monitors their exit, and updates the Job status.
  6. History cleanup is automatic: the CronJob controller deletes Jobs older than successfulJobsHistoryLimit or failedJobsHistoryLimit.

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> shows LastScheduleTime. If it is nil and status conditions show FailedCreate, 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 jobs shows COMPLETIONS: 0/1. kubectl describe job <name> shows Conditions: 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: successfulJobsHistoryLimit and failedJobsHistoryLimit are not set or set too high.
  • Fix: Always specify limits. For high-frequency jobs, set successfulJobsHistoryLimit: 1 and failedJobsHistoryLimit: 1. You can manually delete older jobs with kubectl 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: OnFailure inside the Job template — the default Pod restart policy for Jobs is Never, but CronJob templates must use OnFailure to allow retries.
  • Using a cron expression that fires every second (e.g., * * * * every minute is correct, * * * * * is invalid and will be rejected).
  • Not setting successfulJobsHistoryLimit and failedJobsHistoryLimit — 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.timeZone is set (Kubernetes 1.27+).
  • Omitting the concurrencyPolicy — overlapping jobs can run simultaneously by default, which may cause data races or resource exhaustion.

Variations

  1. Use concurrencyPolicy: Forbid to skip a run if the previous one is still active — ideal for backup jobs that must not overlap.
  2. Inject environment variables from Secrets or ConfigMaps to avoid hardcoding credentials inside the Job template.
  3. 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_dump and 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/v1 Job manifest.
  • The schedule uses standard POSIX cron syntax with optional timezone support (Kubernetes 1.27+).
  • Set successfulJobsHistoryLimit and failedJobsHistoryLimit to avoid accumulating thousands of completed Job objects.
  • Use concurrencyPolicy: Forbid or Replace when 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.