Warrior, until now almost everything we've deployed has been a long-running application:
Deployment
↓
Pod
↓
Application keeps runningBut many real applications also need tasks that run, finish, and stop.
Examples:
For these, Kubernetes provides:
Job
CronJobThis distinction is extremely important.
A Deployment wants Pods to keep running.
Pod starts
↓
Application runs
↓
Pod keeps runningExample:
Node.js API
Next.js
NginxA Job wants a task to complete successfully.
Pod starts
↓
Task executes
↓
Task finishes
↓
Pod completesExample:
Database migrationSo:
Deployment = keep application running
Job = run task to completion
Suppose your Node.js application has:
npm run migrateYou don't want:
Migration Pod
↓
Run foreverYou want:
Migration Pod
↓
Run migration
↓
Exit successfully
↓
CompletedThat's a Job.
A Kubernetes Job creates one or more Pods and ensures that a specified number of them successfully complete.
Simple architecture:
Job
│
▼
Pod
│
▼
Container
│
▼
Task
│
▼
Exit 0
│
▼
CompletedapiVersion: batch/v1
kind: Job
metadata:
name: hello-job
spec:
template:
spec:
restartPolicy: Never
containers:
- name: hello
image: busybox:1.36
command:
- sh
- -c
- echo "Hello from Kubernetes Job"Apply:
kubectl apply -f job.yamlCheck:
kubectl get jobsYou might see:
NAME COMPLETIONS DURATION AGE
hello-job 1/1 2s 10sCheck Pods:
kubectl get podsYou may see:
hello-job-xxxxx 0/1 CompletedrestartPolicy?#A Job Pod must use an appropriate restart policy.
Commonly:
restartPolicy: Neveror:
restartPolicy: OnFailureFor Jobs, Always isn't appropriate because the workload is supposed to terminate after completion.
Never vs OnFailure#Never#If the container fails:
Container fails
↓
Pod fails
↓
Job can create another PodOnFailure#The container can be restarted within the Pod when it fails.
Think:
Never
→ new Pod may be created
OnFailure
→ container can restart in same PodThe Job controller still tracks successful completion.
A Job may go through:
Created
↓
Pod created
↓
Running
↓
Task executes
↓
Success
↓
CompletedIf it fails:
Pod
↓
Task fails
↓
Job retries according to configurationSuppose you have:
my-node-api:1.0and the Docker image contains:
{
"scripts": {
"migrate": "node scripts/migrate.js"
}
}Job:
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migration
image: my-node-api:1.0
command:
- npm
- run
- migrateFlow:
Job
↓
Node.js container
↓
npm run migrate
↓
Database migration
↓
Exit 0
↓
Job CompletedRun:
kubectl get jobsExample:
NAME COMPLETIONS DURATION
db-migration 1/1 5sMeaning:
1 successful completion
out of 1 requiredkubectl describe job db-migrationCheck logs:
kubectl logs job/db-migrationYou can also inspect the Pod:
kubectl get podsSuppose:
Database unavailableYour migration exits:
exit code 1The Job isn't successful.
Job
↓
Pod
↓
Migration
↓
❌ FailedKubernetes can retry according to the Job configuration.
backoffLimit#You can control how many failures are tolerated before the Job is considered failed.
Example:
spec:
backoffLimit: 4Conceptually:
Attempt 1 → Fail
Attempt 2 → Fail
Attempt 3 → Fail
Attempt 4 → Fail
...
Job eventually marked FailedThe exact retry behavior includes controller backoff, so don't interpret backoffLimit simply as a fixed number of seconds or always exactly one new Pod per failure.
apiVersion: batch/v1
kind: Job
metadata:
name: data-processing
spec:
backoffLimit: 3
template:
spec:
restartPolicy: Never
containers:
- name: processor
image: my-data-processor:1.0
command:
- python
- process.pyMeaning:
Run processing task
↓
If it fails
↓
Retry within configured Job behavior
↓
After failure limit
↓
Job = Failedcompletions#Suppose you need:
10 successful executionsYou can specify:
completions: 10Now Kubernetes wants:
10 successful completionsConceptually:
Job
├── Task 1 ✅
├── Task 2 ✅
├── Task 3 ✅
├── ...
└── Task 10 ✅Then:
10/10 Completedparallelism#Suppose you have:
completions: 10
parallelism: 3This means:
Need:
10 successful completions
Run at most approximately:
3 active Pods at a timeConceptually:
Batch 1:
Pod 1
Pod 2
Pod 3
Batch 2:
Pod 4
Pod 5
Pod 6
Batch 3:
Pod 7
Pod 8
Pod 9
Batch 4:
Pod 10This is useful for batch processing.
This is particularly relevant to your RAG/document workloads.
Suppose:
150,000 documentsneed embedding.
Instead of:
One huge processyou could divide the workload:
150,000 documents
↓
10,000-document batches
↓
Jobs
↓
Multiple workersFor example:
Job
├── Worker 1
├── Worker 2
├── Worker 3
└── Worker 4Each worker processes a portion of the dataset.
parallelism vs completions#Remember:
completions
→ How many successful tasks are required?
parallelism
→ How many can run simultaneously?Example:
completions: 100
parallelism: 10Means:
Need 100 successful completions
Maximum desired active parallelism = 10apiVersion: batch/v1
kind: Job
metadata:
name: document-processing
spec:
completions: 100
parallelism: 10
backoffLimit: 5
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: document-worker:1.0Architecture:
Job
│
┌──────────┼──────────┐
▼ ▼ ▼
Worker 1 Worker 2 Worker 3
...
│
▼
100 completionsFor some batch workloads, you may want each Pod to receive a unique completion index.
For example:
0
1
2
3
...
99You can use:
completionMode: IndexedThen each worker can know:
"I am worker #37"This can be useful for partitioning workloads.
For example:
Worker 0 → Documents 0–999
Worker 1 → Documents 1000–1999
Worker 2 → Documents 2000–2999Your application needs to use the index correctly.
What if your task hangs forever?
You don't want:
Job
↓
Running
↓
Running
↓
Running
↓
Running foreverYou can configure:
activeDeadlineSeconds: 600Meaning:
The Job has a maximum active duration of about 600 seconds.
After the deadline, the Job is terminated and marked failed.
ttlSecondsAfterFinished#Completed Jobs can remain in the cluster.
Over time:
Job 1
Job 2
Job 3
...
Job 10000That's undesirable.
You can configure cleanup:
ttlSecondsAfterFinished: 300Meaning Kubernetes can clean up the finished Job after the configured TTL.
Example:
Job completes
↓
Wait 300 seconds
↓
Job cleaned upThis helps avoid accumulating large numbers of historical Job objects.
apiVersion: batch/v1
kind: Job
metadata:
name: document-worker
spec:
completions: 10
parallelism: 3
backoffLimit: 4
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 600
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: document-worker:1.0
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"This is much closer to a real production batch workload.
A Job runs when you create it.
But what if you want:
Every day at 2 AMor:
Every 15 minutesor:
Every SundayYou need:
CronJob
Think:
CronJob
│
│ schedule
▼
Job
│
▼
Pod
│
▼
TaskA CronJob doesn't directly run your application container forever.
It creates Jobs according to a schedule.
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-report
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: report
image: report-generator:1.0Schedule:
0 2 * * *means:
Every day at 2:00 AMThe actual timezone behavior should be explicitly configured where needed; don't assume your cluster's local timezone.
Traditional cron has five fields:
┌──────── minute
│ ┌────── hour
│ │ ┌──── day of month
│ │ │ ┌── month
│ │ │ │ ┌ day of week
│ │ │ │ │
* * * * ** * * * *0 * * * *0 2 * * *0 3 * * 0*/15 * * * *0 9 * * 1Imagine:
PostgreSQLYou want a backup every night.
Architecture:
CronJob
│
│ every day 2 AM
▼
Job
│
▼
Backup Pod
│
▼
pg_dump
│
▼
Object StorageFor example:
apiVersion: batch/v1
kind: CronJob
metadata:
name: postgres-backup
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: backup
image: postgres:17
command:
- sh
- -c
- |
pg_dump "$DATABASE_URL" > /backup/db.sqlIn production, you'd also need proper secret handling, durable backup storage, retention, encryption, failure monitoring, and restore testing.
Suppose your application stores temporary records.
You could run:
Every night
↓
Cleanup expired recordsCronJob:
apiVersion: batch/v1
kind: CronJob
metadata:
name: cleanup
spec:
schedule: "0 1 * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: cleanup
image: my-node-api:1.0
command:
- npm
- run
- cleanupSuppose your AI system needs to process documents every night:
11 PM
↓
Fetch new documents
↓
Extract text
↓
Generate embeddings
↓
Store vectorsCronJob:
CronJob
↓
Job
↓
Workers
↓
Documents
↓
Embeddings
↓
Vector DBThis is a very realistic Kubernetes use case.
concurrencyPolicy#What if your CronJob runs every 10 minutes but a previous Job takes 30 minutes?
You could have:
10:00 → Job A starts
10:10 → Job B starts
10:20 → Job C starts
10:30 → Job D startsNow multiple copies overlap.
Sometimes that's desired.
Sometimes it's dangerous.
Kubernetes provides:
concurrencyPolicy: ForbidMeaning:
Don't start a new Job while the previous one is still running.
Three important values:
Allow
Forbid
ReplaceMultiple Jobs can overlap.
Job A ─────────
Job B ─────────
Job C ─────────Don't start another while one is active.
Job A ─────────
Job B
↓
Skipped because A is activeA newer scheduled Job can replace the currently running one according to CronJob semantics.
This can be useful when only the latest scheduled execution matters.
startingDeadlineSeconds#What if the scheduled execution is missed?
For example:
Scheduled: 2:00 AM
Controller couldn't schedule/run it normallyYou can configure:
startingDeadlineSeconds: 300This specifies a deadline for starting a missed Job.
successfulJobsHistoryLimit#CronJobs can retain historical Jobs.
You can configure:
successfulJobsHistoryLimit: 3Meaning:
Keep a limited number of successful Job histories.
Similarly:
failedJobsHistoryLimit: 3helps control retained failed Job history.
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-processing
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 3
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 600
template:
spec:
restartPolicy: Never
containers:
- name: processor
image: document-processor:1.0
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"This is a useful production-style template.
Remember this:
CronJob
│
schedule
│
▼
Job
│
▼
Pod
│
▼
Container
│
▼
Task
│
┌─────┴─────┐
Success Failure
│ │
▼ ▼
Completed RetryThis table is worth memorizing.
| Feature | Deployment | Job | CronJob |
|---|---|---|---|
| Long-running | ✅ | ❌ | ❌ |
| Runs to completion | ❌ | ✅ | ✅ |
| Scheduled | ❌ | ❌ | ✅ |
| Typical use | API | Migration | Daily backup |
| Creates Pods | ✅ | ✅ | Via Job |
| Keeps replicas running | ✅ | No | No |
Simple mental model:
Deployment
→ "Keep this application alive."
Job
→ "Do this task and finish."
CronJob
→ "Do this task on a schedule."Imagine your application:
Next.js
Node.js API
PostgreSQL
RedisYou could have:
Deployment
├── Next.js
└── Node.js API
Stateful workload / managed DB
└── PostgreSQL
Deployment
└── Redis
Job
└── Database migration
CronJob
├── Daily cleanup
├── Daily report
└── Scheduled data processingThis is how different Kubernetes workload types fit together.
Suppose your migration requires:
DATABASE_URLDon't hardcode it.
Use a Secret:
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: DATABASE_URLThen:
Job
↓
Secret
↓
DATABASE_URL
↓
MigrationThis connects Chapter 9 with Chapter 18.
Similarly:
envFrom:
- configMapRef:
name: app-configNow your batch worker can consume normal configuration without embedding it in the image.
Batch jobs also need resource settings:
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"Why?
Because Jobs are still Pods.
Therefore:
Job
↓
Pod
↓
Scheduler
↓
Resources matterAll the scheduling concepts from Chapter 17 still apply.
Here's a useful connection.
Suppose:
100 batch Podsare created.
Your cluster doesn't have enough capacity.
Batch Pods
↓
Pending
↓
Cluster Autoscaler
↓
More Nodes
↓
Scheduler
↓
Batch Pods runSo Cluster Autoscaler isn't only useful for HTTP traffic.
It can also help batch workloads when configured appropriately.
Suppose:
Job
↓
Pod
↓
Processing
↓
Node crashesThe Job controller can create replacement work according to the Job's configuration and completion state.
This is one reason Jobs are preferable to manually creating one-off Pods for important batch tasks.
You could create:
Pod
↓
Run migration
↓
DoneBut a standalone Pod doesn't give you the same Job semantics around:
So:
For batch workloads, use Jobs rather than manually managed one-off Pods.
A workload controller that runs Pods until a specified number of successful completions is achieved.
Deployment → long-running
Job → finite taskA controller that creates Jobs according to a schedule.
completions?#The desired number of successful completions.
parallelism?#The desired maximum number of active Pods for the Job at a time.
backoffLimit?#Controls how many retries/failures are allowed before the Job is considered failed.
concurrencyPolicy?#Controls how overlapping executions of a CronJob are handled.
Forbid?#Don't start a new Job while the previous scheduled Job is still active.
ttlSecondsAfterFinished?#Allows finished Job resources to be automatically cleaned up after a configured delay.
Yes.
Just like normal Pods, Job Pods can consume ConfigMaps and Secrets.
Deployment
↓
Long-running applicationJob
↓
One-time/batch task
↓
CompletionCronJob
↓
Schedule
↓
Job
↓
Pod
↓
Taskcompletions
parallelism
backoffLimit
activeDeadlineSeconds
ttlSecondsAfterFinished
completionModeschedule
concurrencyPolicy
startingDeadlineSeconds
successfulJobsHistoryLimit
failedJobsHistoryLimitImagine you're running your NextGen Blog / AI platform:
Kubernetes Cluster
│
┌────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
Deployment Job CronJob
│ │ │
▼ ▼ schedule
Node.js API DB Migration │
│ │ ▼
▼ ▼ Job
Service Completed │
│ ▼
▼ Pod
Users │
▼
CleanupAnd the larger architecture is now:
INTERNET
│
▼
Ingress
│
▼
Service
│
┌─────────┴─────────┐
▼ ▼
Deployment HPA
│ │
Pods More/Fewer Pods
│
▼
Scheduler
│
▼
Nodes
│
▲
│
Cluster Autoscaler
Separate workloads:
│
├── Job → one-time task
│
└── CronJob → scheduled taskDeployment = Run
Job = Finish
CronJob = Schedule
We'll now enter the Security phase, covering:
Authentication
Authorization
RBAC
ServiceAccounts
Secrets security
Roles
RoleBindings
ClusterRoles
Least privilege
SecurityContext
Pod securityAnd we'll build a realistic example of securing a Node.js API + PostgreSQL + Redis Kubernetes application.
Mark it as complete to track your mastery.