Learning Kubernetes - Episode 15 - Introduction and Explanation of CronJob

Learning Kubernetes - Episode 15 - Introduction and Explanation of CronJob

In this episode, we'll discuss Kubernetes CronJob, a controller for running Jobs on a time-based schedule. We'll learn how CronJob enables scheduled tasks, recurring operations, and automated maintenance in Kubernetes.

Arman Dwi Pangestu
Arman Dwi PangestuMarch 18, 2026
0 views
9 min read

Introduction

Note

If you want to read the previous episode, you can click the Episode 14 thumbnail below

Episode 14Episode 14

In the previous episode, we learned about Job, which runs tasks to completion. In episode 15, we'll discuss CronJob, which builds on Job to provide scheduled, recurring task execution.

Note: Here I'll be using a Kubernetes Cluster installed through K3s.

If you're familiar with Unix/Linux cron, CronJob in Kubernetes works similarly - it creates Jobs on a repeating schedule. Think of it as your cluster's task scheduler for automated backups, reports, cleanups, and any recurring operations.

What Is CronJob?

A CronJob creates Jobs on a time-based schedule. It uses the same cron format as Unix/Linux systems to define when Jobs should run. CronJob manages the lifecycle of Jobs, creating them at scheduled times and cleaning up old Jobs.

Think of CronJob like a cron daemon in Linux - it runs tasks at specified times automatically. In Kubernetes, CronJob creates Job objects on schedule, which then create Pods to execute the actual work.

Key characteristics of CronJob:

  • Scheduled execution - Runs Jobs at specified times using cron syntax
  • Recurring tasks - Automatically creates Jobs on schedule
  • Job management - Manages Job creation and cleanup
  • Concurrency control - Controls how concurrent Jobs are handled
  • History limits - Automatically cleans up old Jobs
  • Timezone support - Can run in specific timezones
  • Suspend capability - Can pause scheduled execution

Why Do We Need CronJob?

CronJob is designed for recurring tasks that need to run on a schedule:

  • Scheduled backups - Daily database backups, weekly full backups
  • Report generation - Daily reports, monthly summaries
  • Data cleanup - Remove old logs, temporary files, expired data
  • Data synchronization - Sync data between systems periodically
  • Health checks - Periodic system health verification
  • Certificate renewal - Automated certificate management
  • Cache warming - Pre-populate caches before peak hours
  • Batch processing - Process accumulated data at specific times
  • Monitoring tasks - Periodic checks and alerts

Without CronJob, you would need to:

  • Manually create Jobs at scheduled times
  • Set up external schedulers
  • Manage Job cleanup yourself
  • Handle timezone conversions manually

CronJob vs Job

Let's understand the key differences:

AspectCronJobJob
ExecutionScheduled, recurringOne-time
ScheduleCron syntaxImmediate
Job creationAutomatic on scheduleManual
Use caseRecurring tasksOne-time tasks
ManagementCreates and manages JobsRuns Pods directly
CleanupAutomatic with history limitsManual or TTL

Example scenario:

  • CronJob: Run database backup every day at 2 AM
  • Job: Run database migration once during deployment

Cron Schedule Syntax

CronJob uses standard cron format with 5 fields:

plaintext
┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday)
│ │ │ │ │
│ │ │ │ │
* * * * *

Common Schedule Examples

Every minute:

plaintext
* * * * *

Every hour at minute 0:

plaintext
0 * * * *

Every day at 2:30 AM:

plaintext
30 2 * * *

Every Monday at 9:00 AM:

plaintext
0 9 * * 1

Every 15 minutes:

plaintext
*/15 * * * *

Every 6 hours:

plaintext
0 */6 * * *

First day of every month at midnight:

plaintext
0 0 1 * *

Every weekday at 6:00 PM:

plaintext
0 18 * * 1-5

Every Sunday at 3:00 AM:

plaintext
0 3 * * 0

Twice a day (6 AM and 6 PM):

plaintext
0 6,18 * * *

Creating a CronJob

Let's create a basic CronJob:

Example 1: Basic CronJob

Create a file named cronjob-basic.yml:

Kubernetescronjob-basic.yml
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

This CronJob runs every minute.

Apply the configuration:

Kubernetesbash
sudo kubectl apply -f cronjob-basic.yml

Verify the CronJob is created:

Kubernetesbash
sudo kubectl get cronjobs

Or use the shorthand:

Kubernetesbash
sudo kubectl get cj

Output:

Kubernetesbash
NAME             SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
hello-cronjob    */1 * * * *   False     0        <none>          10s

Wait a minute and check the Jobs created:

Kubernetesbash
sudo kubectl get jobs

Output:

Kubernetesbash
NAME                      COMPLETIONS   DURATION   AGE
hello-cronjob-28458640    1/1           5s         45s
hello-cronjob-28458641    1/1           4s         5s

Check the Pods:

Kubernetesbash
sudo kubectl get pods

View logs from a completed Pod:

Kubernetesbash
sudo kubectl logs hello-cronjob-28458640-abc12

Output:

Kubernetesbash
Sun Mar  1 10:00:00 UTC 2026
Hello from CronJob!

CronJob Concurrency Policy

Control how CronJob handles overlapping executions:

Allow (Default)

Allows concurrent Jobs to run:

Kubernetescronjob-allow.yml
apiVersion: batch/v1
kind: CronJob
metadata:
    name: concurrent-cronjob
spec:
    schedule: "*/1 * * * *"
    concurrencyPolicy: Allow
    jobTemplate:
        spec:
            template:
                spec:
                    containers:
                        - name: task
                          image: busybox:1.36
                          command:
                              - /bin/sh
                              - -c
                              - echo "Starting"; sleep 120; echo "Done"
                    restartPolicy: OnFailure

Behavior:

  • Multiple Jobs can run simultaneously
  • New Job starts even if previous Job is still running
  • Good for independent tasks

Forbid

Prevents concurrent Jobs:

Kubernetescronjob-forbid.yml
apiVersion: batch/v1
kind: CronJob
metadata:
    name: sequential-cronjob
spec:
    schedule: "*/1 * * * *"
    concurrencyPolicy: Forbid
    jobTemplate:
        spec:
            template:
                spec:
                    containers:
                        - name: task
                          image: busybox:1.36
                          command:
                              - /bin/sh
                              - -c
                              - echo "Starting"; sleep 120; echo "Done"
                    restartPolicy: OnFailure

Behavior:

  • Skips new Job if previous Job is still running
  • Prevents overlapping executions
  • Good for tasks that shouldn't run concurrently

Replace

Replaces currently running Job with new one:

Kubernetescronjob-replace.yml
apiVersion: batch/v1
kind: CronJob
metadata:
    name: replace-cronjob
spec:
    schedule: "*/1 * * * *"
    concurrencyPolicy: Replace
    jobTemplate:
        spec:
            template:
                spec:
                    containers:
                        - name: task
                          image: busybox:1.36
                          command:
                              - /bin/sh
                              - -c
                              - echo "Starting"; sleep 120; echo "Done"
                    restartPolicy: OnFailure

Behavior:

  • Cancels currently running Job
  • Starts new Job in its place
  • Good for tasks where latest execution is most important

History Limits

Control how many completed and failed Jobs to keep:

Kubernetescronjob-history.yml
apiVersion: batch/v1
kind: CronJob
metadata:
    name: history-cronjob
spec:
    schedule: "*/5 * * * *"
    successfulJobsHistoryLimit: 3
    failedJobsHistoryLimit: 1
    jobTemplate:
        spec:
            template:
                spec:
                    containers:
                        - name: task
                          image: busybox:1.36
                          command: ["echo", "Task completed"]
                    restartPolicy: OnFailure

This CronJob:

  • Keeps last 3 successful Jobs
  • Keeps last 1 failed Job
  • Automatically deletes older Jobs

Default values:

  • successfulJobsHistoryLimit: 3
  • failedJobsHistoryLimit: 1

Starting Deadline

Set a deadline for starting Jobs:

Kubernetescronjob-deadline.yml
apiVersion: batch/v1
kind: CronJob
metadata:
    name: deadline-cronjob
spec:
    schedule: "0 2 * * *"
    startingDeadlineSeconds: 3600
    jobTemplate:
        spec:
            template:
                spec:
                    containers:
                        - name: task
                          image: busybox:1.36
                          command: ["echo", "Task completed"]
                    restartPolicy: OnFailure

This CronJob:

  • Scheduled to run at 2:00 AM
  • Must start within 1 hour (3600 seconds)
  • If missed, counts as failed schedule

Use case: If cluster was down during scheduled time, Job can still start when cluster comes back up (within deadline).

Suspending CronJob

Temporarily pause CronJob execution:

Kubernetescronjob-suspend.yml
apiVersion: batch/v1
kind: CronJob
metadata:
    name: suspended-cronjob
spec:
    schedule: "*/1 * * * *"
    suspend: true
    jobTemplate:
        spec:
            template:
                spec:
                    containers:
                        - name: task
                          image: busybox:1.36
                          command: ["echo", "Task completed"]
                    restartPolicy: OnFailure

With suspend: true, CronJob won't create new Jobs.

Suspend an existing CronJob:

Kubernetesbash
sudo kubectl patch cronjob hello-cronjob -p '{"spec":{"suspend":true}}'

Resume a suspended CronJob:

Kubernetesbash
sudo kubectl patch cronjob hello-cronjob -p '{"spec":{"suspend":false}}'

Timezone Support

Specify timezone for schedule (Kubernetes 1.25+):

Kubernetescronjob-timezone.yml
apiVersion: batch/v1
kind: CronJob
metadata:
    name: timezone-cronjob
spec:
    schedule: "0 9 * * *"
    timeZone: "America/New_York"
    jobTemplate:
        spec:
            template:
                spec:
                    containers:
                        - name: task
                          image: busybox:1.36
                          command: ["echo", "Good morning!"]
                    restartPolicy: OnFailure

This runs at 9:00 AM Eastern Time.

Important

Important: Timezone support requires Kubernetes 1.25 or later. Without timeZone field, schedule uses controller manager's timezone (usually UTC).

Viewing CronJob Details

To see detailed information about a CronJob:

Kubernetesbash
sudo kubectl describe cronjob hello-cronjob

Output:

Kubernetesbash
Name:                          hello-cronjob
Namespace:                     default
Labels:                        <none>
Annotations:                   <none>
Schedule:                      */1 * * * *
Concurrency Policy:            Allow
Suspend:                       False
Successful Job History Limit:  3
Failed Job History Limit:      1
Starting Deadline Seconds:     <unset>
Selector:                      <unset>
Parallelism:                   <unset>
Completions:                   <unset>
Pod Template:
  Labels:  <none>
  Containers:
   hello:
    Image:      busybox:1.36
    Command:
      /bin/sh
      -c
      date; echo "Hello from CronJob!"
    Environment:     <none>
    Mounts:          <none>
  Volumes:           <none>
Last Schedule Time:  Sun, 01 Mar 2026 10:05:00 +0000
Active Jobs:         <none>
Events:
  Type    Reason            Age   From                Message
  ----    ------            ----  ----                -------
  Normal  SuccessfulCreate  5m    cronjob-controller  Created job hello-cronjob-28458640
  Normal  SuccessfulCreate  4m    cronjob-controller  Created job hello-cronjob-28458641
  Normal  SuccessfulCreate  3m    cronjob-controller  Created job hello-cronjob-28458642

Practical Examples

Example 1: Daily Database Backup

Kubernetesbackup-cronjob.yml
apiVersion: batch/v1
kind: CronJob
metadata:
    name: database-backup
    labels:
        app: backup
        type: database
spec:
    schedule: "0 2 * * *"
    timeZone: "UTC"
    concurrencyPolicy: Forbid
    successfulJobsHistoryLimit: 7
    failedJobsHistoryLimit: 3
    jobTemplate:
        spec:
            backoffLimit: 2
            activeDeadlineSeconds: 3600
            template:
                metadata:
                    labels:
                        app: backup
                        type: database
                spec:
                    containers:
                        - name: backup
                          image: postgres:15-alpine
                          command:
                              - /bin/sh
                              - -c
                              - |
                                  BACKUP_FILE="/backup/db-backup-$(date +%Y%m%d-%H%M%S).sql"
                                  pg_dump -h $DB_HOST -U $DB_USER -d $DB_NAME > $BACKUP_FILE
                                  gzip $BACKUP_FILE
                                  echo "Backup completed: ${BACKUP_FILE}.gz"
                          env:
                              - name: DB_HOST
                                value: "postgres-service"
                              - name: DB_USER
                                valueFrom:
                                    secretKeyRef:
                                        name: db-credentials
                                        key: username
                              - name: DB_NAME
                                value: "production"
                              - name: PGPASSWORD
                                valueFrom:
                                    secretKeyRef:
                                        name: db-credentials
                                        key: password
                          volumeMounts:
                              - name: backup-storage
                                mountPath: /backup
                    volumes:
                        - name: backup-storage
                          persistentVolumeClaim:
                              claimName: backup-pvc
                    restartPolicy: OnFailure

This CronJob:

  • Runs daily at 2:00 AM UTC
  • Prevents concurrent backups
  • Keeps 7 successful backups
  • Compresses backup files
  • Stores in persistent volume

Example 2: Hourly Log Cleanup

Kubernetescleanup-cronjob.yml
apiVersion: batch/v1
kind: CronJob
metadata:
    name: log-cleanup
spec:
    schedule: "0 * * * *"
    concurrencyPolicy: Forbid
    successfulJobsHistoryLimit: 3
    failedJobsHistoryLimit: 1
    jobTemplate:
        spec:
            template:
                spec:
                    containers:
                        - name: cleanup
                          image: busybox:1.36
                          command:
                              - /bin/sh
                              - -c
                              - |
                                  echo "Starting log cleanup..."
                                  find /logs -name "*.log" -mtime +7 -delete
                                  echo "Cleanup completed"
                          volumeMounts:
                              - name: logs
                                mountPath: /logs
                    volumes:
                        - name: logs
                          hostPath:
                              path: /var/log/app
                    restartPolicy: OnFailure

This CronJob:

  • Runs every hour
  • Deletes logs older than 7 days
  • Prevents concurrent cleanup operations

Example 3: Weekly Report Generation

Kubernetesreport-cronjob.yml
apiVersion: batch/v1
kind: CronJob
metadata:
    name: weekly-report
spec:
    schedule: "0 8 * * 1"
    timeZone: "America/New_York"
    concurrencyPolicy: Forbid
    successfulJobsHistoryLimit: 4
    failedJobsHistoryLimit: 2
    jobTemplate:
        spec:
            backoffLimit: 1
            template:
                spec:
                    containers:
                        - name: report-generator
                          image: python:3.11-slim
                          command:
                              - python
                              - -c
                              - |
                                  import datetime
                                  print("Generating weekly report...")
                                  print(f"Report date: {datetime.datetime.now()}")
                                  # Generate report logic here
                                  print("Report generated successfully")
                          env:
                              - name: DATABASE_URL
                                valueFrom:
                                    secretKeyRef:
                                        name: app-secrets
                                        key: database-url
                              - name: SMTP_HOST
                                value: "smtp.example.com"
                          resources:
                              requests:
                                  memory: "512Mi"
                                  cpu: "500m"
                              limits:
                                  memory: "1Gi"
                                  cpu: "1000m"
                    restartPolicy: OnFailure

This CronJob:

  • Runs every Monday at 8:00 AM Eastern Time
  • Generates weekly reports
  • Sets resource limits for report generation

Example 4: Cache Warming

Kubernetescache-warming-cronjob.yml
apiVersion: batch/v1
kind: CronJob
metadata:
    name: cache-warmer
spec:
    schedule: "0 6 * * *"
    concurrencyPolicy: Replace
    successfulJobsHistoryLimit: 2
    failedJobsHistoryLimit: 1
    jobTemplate:
        spec:
            activeDeadlineSeconds: 1800
            template:
                spec:
                    containers:
                        - name: warmer
                          image: curlimages/curl:latest
                          command:
                              - /bin/sh
                              - -c
                              - |
                                  echo "Warming cache..."
                                  curl -s http://api-service/api/warm-cache
                                  echo "Cache warmed successfully"
                    restartPolicy: OnFailure

This CronJob:

  • Runs daily at 6:00 AM
  • Warms application cache before peak hours
  • Replaces running Job if new schedule arrives

Example 5: Certificate Check

Kubernetescert-check-cronjob.yml
apiVersion: batch/v1
kind: CronJob
metadata:
    name: certificate-checker
spec:
    schedule: "0 0 * * *"
    concurrencyPolicy: Forbid
    successfulJobsHistoryLimit: 7
    failedJobsHistoryLimit: 3
    jobTemplate:
        spec:
            template:
                spec:
                    containers:
                        - name: checker
                          image: alpine:latest
                          command:
                              - /bin/sh
                              - -c
                              - |
                                  apk add --no-cache openssl
                                  echo "Checking certificates..."
                                  echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
                                  echo "Certificate check completed"
                    restartPolicy: OnFailure

This CronJob:

  • Runs daily at midnight
  • Checks SSL certificate expiration
  • Keeps 7 days of history

Monitoring CronJobs

Check CronJob Status

Kubernetesbash
sudo kubectl get cronjobs

View CronJob Details

Kubernetesbash
sudo kubectl describe cronjob hello-cronjob

List Jobs Created by CronJob

Kubernetesbash
sudo kubectl get jobs --selector=cronjob=hello-cronjob

Watch CronJob Activity

Kubernetesbash
sudo kubectl get cronjobs -w

Check Last Execution Time

Kubernetesbash
sudo kubectl get cronjob hello-cronjob -o jsonpath='{.status.lastScheduleTime}'

View CronJob Events

Kubernetesbash
sudo kubectl get events --sort-by='.lastTimestamp' | grep CronJob

Common Mistakes and Pitfalls

Mistake 1: Incorrect Cron Syntax

Problem: Using wrong cron format or invalid values.

Solution: Validate cron syntax before applying:

bash
# Use online cron validators or test locally
# Correct: */5 * * * * (every 5 minutes)
# Wrong: 5 * * * * (at minute 5 of every hour)

Mistake 2: Not Setting Concurrency Policy

Problem: Multiple Jobs running concurrently when they shouldn't.

Solution: Set appropriate concurrencyPolicy:

Kubernetesyml
spec:
    concurrencyPolicy: Forbid  # For tasks that shouldn't overlap

Mistake 3: No History Limits

Problem: Accumulation of old Jobs consuming resources.

Solution: Set history limits:

Kubernetesyml
spec:
    successfulJobsHistoryLimit: 3
    failedJobsHistoryLimit: 1

Mistake 4: Ignoring Timezone

Problem: Jobs run at wrong times due to timezone confusion.

Solution: Explicitly set timezone (Kubernetes 1.25+):

Kubernetesyml
spec:
    timeZone: "America/New_York"

Mistake 5: No Starting Deadline

Problem: Missed schedules pile up when cluster recovers.

Solution: Set startingDeadlineSeconds:

Kubernetesyml
spec:
    startingDeadlineSeconds: 3600

Mistake 6: Not Setting Resource Limits

Problem: CronJob Pods consume excessive resources.

Solution: Always set resource limits in Job template:

Kubernetesyml
resources:
    requests:
        memory: "256Mi"
        cpu: "250m"
    limits:
        memory: "512Mi"
        cpu: "500m"

Best Practices

Use Appropriate Concurrency Policy

Choose based on task requirements:

Kubernetesyml
spec:
    concurrencyPolicy: Forbid  # For sequential tasks
    # concurrencyPolicy: Allow  # For independent tasks
    # concurrencyPolicy: Replace  # For latest-only tasks

Set History Limits

Prevent Job accumulation:

Kubernetesyml
spec:
    successfulJobsHistoryLimit: 3
    failedJobsHistoryLimit: 1

Configure Starting Deadline

Handle missed schedules gracefully:

Kubernetesyml
spec:
    startingDeadlineSeconds: 3600

Use Timezone Field

Make schedules explicit (Kubernetes 1.25+):

Kubernetesyml
spec:
    timeZone: "UTC"

Set Job Deadlines

Prevent Jobs from running too long:

Kubernetesyml
jobTemplate:
    spec:
        activeDeadlineSeconds: 3600

Add Meaningful Labels

Organize and filter CronJobs:

Kubernetesyml
metadata:
    labels:
        app: backup
        type: database
        schedule: daily

Use Secrets for Credentials

Never hardcode sensitive data:

Kubernetesyml
env:
    - name: PASSWORD
      valueFrom:
          secretKeyRef:
              name: credentials
              key: password

Set Resource Limits

Prevent resource exhaustion:

Kubernetesyml
resources:
    requests:
        memory: "256Mi"
        cpu: "250m"
    limits:
        memory: "512Mi"
        cpu: "500m"

Test Schedule Before Production

Verify cron syntax and timing:

Kubernetesbash
# Test with frequent schedule first
schedule: "*/1 * * * *"  # Every minute for testing
 
# Then change to production schedule
schedule: "0 2 * * *"  # Daily at 2 AM

Monitor CronJob Execution

Set up alerts for failed Jobs:

Kubernetesbash
# Check for failed Jobs
sudo kubectl get jobs --field-selector status.successful=0

Deleting CronJobs

Delete CronJob and Its Jobs

Kubernetesbash
sudo kubectl delete cronjob hello-cronjob

This deletes the CronJob and all Jobs it created.

Delete CronJob but Keep Jobs

Kubernetesbash
sudo kubectl delete cronjob hello-cronjob --cascade=orphan

This deletes only the CronJob, leaving Jobs running.

Troubleshooting CronJobs

CronJob Not Creating Jobs

Check if CronJob is suspended:

Kubernetesbash
sudo kubectl get cronjob hello-cronjob -o jsonpath='{.spec.suspend}'

Check schedule syntax:

Kubernetesbash
sudo kubectl describe cronjob hello-cronjob

Jobs Failing Repeatedly

Check Job logs:

Kubernetesbash
# Get latest Job
JOB=$(kubectl get jobs --sort-by=.metadata.creationTimestamp | tail -1 | awk '{print $1}')
 
# Get Pod from Job
POD=$(kubectl get pods --selector=job-name=$JOB -o jsonpath='{.items[0].metadata.name}')
 
# View logs
sudo kubectl logs $POD

Too Many Old Jobs

Adjust history limits:

Kubernetesbash
sudo kubectl patch cronjob hello-cronjob -p '{"spec":{"successfulJobsHistoryLimit":3}}'

Conclusion

In episode 15, we've explored CronJob in Kubernetes in depth. We've learned what CronJob is, how it builds on Job to provide scheduled execution, and how to use it for recurring tasks.

Key takeaways:

  • CronJob creates Jobs on a time-based schedule using cron syntax
  • Uses standard cron format with 5 fields (minute, hour, day, month, weekday)
  • Three concurrency policies: Allow, Forbid, Replace
  • History limits automatically clean up old Jobs
  • Starting deadline handles missed schedules gracefully
  • Timezone support available in Kubernetes 1.25+
  • Can suspend CronJobs to pause execution
  • Perfect for backups, reports, cleanup, and recurring maintenance
  • Always set resource limits and history limits
  • Use appropriate concurrency policy based on task requirements

CronJob is essential for automating recurring tasks in Kubernetes. By understanding CronJob, you can effectively schedule backups, generate reports, clean up resources, and automate maintenance tasks without external schedulers.

Are you getting a clearer understanding of CronJob in Kubernetes? In the next episode 16, we'll discuss Node Selector, a simple mechanism for controlling Pod placement on specific nodes based on labels. Keep your learning momentum going and look forward to the next episode!

Note

If you want to continue to the next episode, you can click the Episode 16 thumbnail below

Episode 16Episode 16

Related Posts