10.2 Parallelism and Completions: Controlling Concurrency
Right, so you’ve got a job that needs to do a thing. Maybe it’s processing a million images, or sending out a batch of emails. You fire up a Job, and it creates a Pod that chugs along. But what if one Pod isn’t enough? What if you need ten, or a hundred, all working in parallel to chew through a massive work queue? That’s where we get into the real power of Jobs: parallelism and managing how they know they’re done.
This is where the spec.parallelism and spec.completions fields come in, and honestly, their names are a bit of a head-scratcher. They sound similar but control two very different things. Let’s clear that up immediately.
Parallelism: The Throttle
Think of parallelism as the concurrency throttle. It doesn’t define the total number of Pods that will ever be created; it defines the maximum number of Pods running at any given time. It’s the knob you turn to control how hard you’re going to hammer your database, external API, or whatever shared resource your Pods are using.
You set .spec.completions to 10 and .spec.parallelism to 3? The Job controller will ensure that only 3 Pods are running simultaneously. As soon as one finishes successfully, it’ll spin up another until 10 have completed. It’s a work queue with a controlled number of workers.
This is your first line of defense against self-inflicted Distributed Denial-of-Service attacks. Here’s a Job that processes items from a queue, but we don’t want more than 5 workers stomping on each other:
apiVersion: batch/v1
kind: Job
metadata:
name: queue-processor
spec:
completions: 50 # Process 50 items total
parallelism: 5 # But only ever use 5 workers at a time
template:
spec:
containers:
- name: worker
image: my-company/queue-worker:latest
# ... your container spec here
restartPolicy: OnFailure
Completions: The Finish Line
The completions field is much simpler: it’s the number of successful Pod executions required for the Job itself to be marked as Complete. Each Pod that finishes successfully increments a counter. When the counter hits the completions value, the Job is done. Kaput. Finished.
If you don’t set completions at all, it defaults to 1. The Job runs one Pod to completion and calls it a day. This is the most common simple case.
But here’s the powerful part: if you do set completions, each Pod gets a unique index, passed in the JOB_COMPLETION_INDEX environment variable. This is your golden ticket for splitting work. You can have each Pod work on a different slice of data.
apiVersion: batch/v1
kind: Job
metadata:
name: data-slicer
spec:
completions: 10 # We want 10 slices processed
parallelism: 2 # But we'll be polite and only run 2 at a time
template:
spec:
containers:
- name: worker
image: my-company/data-processor:latest
env:
- name: JOB_COMPLETION_INDEX
valueFrom:
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
command: ["/bin/process_data.sh"]
args: ["--slice=$(JOB_COMPLETION_INDEX)"]
restartPolicy: OnFailure
The process_data.sh script could use that index to grab a specific file from a volume (data_slice_${INDEX}.csv) or process a specific segment of a database. It’s a beautifully simple pattern for embarrassingly parallel problems.
The IndexedJob: Making It Official
That pattern of using the JOB_COMPLETION_INDEX is so common that Kubernetes formalized it with the Indexed completion mode (the alternative is the default, unimaginatively named “NonIndexed” mode). You just set completionMode: Indexed on the Job spec, and Kubernetes automatically creates a PVC for you (well, it can, but that’s a whole other can of worms) and handles the index tracking more explicitly. For most purposes, using the environment variable as shown above works perfectly fine and is less magic.
The Pitfalls: Where This All Goes Pear-Shaped
The Black Hole Pod: Your
completionsis set to 10. What happens if one Pod enters a broken state where it just runs forever, never succeeding or failing? It just… runs. Your Job will be stuck at 9/10 completions forever, because the Job controller won’t exceed theparallelismlimit to start a replacement. You must set active deadlines (.spec.activeDeadlineSeconds) on your Jobs to avoid this. It’s a safety net.The Thundering Herd: Setting
parallelismtoo high is a classic rookie mistake. You’ll spin up 100 Pods that all immediately try to acquire a lock on the same database row or hit the same rate-limited API endpoint. They’ll all fail spectacularly at the same time. Start low, monitor your external dependencies, and ramp up.Ignoring Restarts: A Pod failing and being restarted counts as a single completion attempt. If your Pod keeps failing and getting restarted (within the
backoffLimit), it hasn’t yet contributed a success to thecompletionscount. Keep yourbackoffLimitlow and your logging good to catch flaky pods quickly.
The real art is in balancing these knobs. You use completions to define the scope of the work and parallelism to control the ferocity with which you attack it. Get it right, and you’ve got a scalable, resilient batch processing system. Get it wrong, and you’re just creating a very expensive, very confusing mess.