Singletons and disruptions
Usually more copies are better. But some jobs must be done by exactly one copy at a time: send the daily newsletter once, not three times. And the opposite worry: when you take a node down for maintenance, you want enough copies to keep running. This chapter covers both: singletons, and PodDisruptionBudgets.
When "only one" matters
Imagine the posts service gets a new feature: every night at 08:00 it emails "today's top posts" to all users. With 3 replicas, three Pods wake up at 08:00 and every user gets three emails. Other examples:
- A worker that reads events in strict order, where two readers would process things twice.
- A job that talks to an outside system allowing only one connection.
- A scheduler inside your app ("every 5 minutes, clean up old sessions").
There are two ways to guarantee one active copy: from the outside (Kubernetes runs only one) or from the inside (many run, but they agree on one boss).
Option 1: let Kubernetes keep it at one
The easiest fix: move the nightly email into its own small workload with one replica. Kubernetes keeps exactly one running and replaces it if it crashes. Your app code stays simple.
If the task runs on a schedule, an even better fit is a CronJob, which starts one Pod at 08:00 and lets it finish (chapter 36).
The replicas: 1 trap
"One replica" sounds like "never two". It is not, with a normal Deployment. Pick an update strategy and see:
08:00:00 newsletter-old Running
08:00:01 newsletter-new Starting ← rollout begins (maxSurge)
08:00:05 newsletter-new Running
newsletter-old Running ← two copies at once!
08:00:06 newsletter-old TerminatingA rolling update starts the new Pod before stopping the old one (chapter 10). For a few seconds, two copies run. If that is when the email goes out, people get it twice.
spec:
replicas: 1
strategy:
type: Recreate # stop the old Pod first, then start the new oneNever two at the same time during a deploy. The price is a short gap with zero copies, which is fine for a background job.
kind: StatefulSet
spec:
replicas: 1The strictest option. A StatefulSet will not start a new newsletter-0 until it is sure the old one is gone, even if its node vanished. "At most one" is guaranteed, at the cost of possibly waiting longer (chapter 38).
Option 2: leader election inside the app
Sometimes you want several copies running, so a standby can take over in seconds, but only one may act. They hold an election using a shared lock. In Kubernetes that lock is a small object called a Lease. It is like a library book: whoever has it borrowed is the leader, and the loan must be renewed regularly or someone else can take it.
posts-a: try to take Lease "newsletter-leader" → success, I am leader posts-b: try → already held by posts-a, wait posts-c: try → already held by posts-a, waitAll three start. Only the first to grab the Lease becomes leader.
apiVersion: coordination.k8s.io/v1 kind: Lease metadata: name: newsletter-leader spec: holderIdentity: posts-a leaseDurationSeconds: 15 renewTime: "2026-10-20T08:00:10Z"The Lease records who holds it and when they last renewed. The leader renews every few seconds.
posts-a: crashes ❌ (stops renewing) ...15 seconds pass... posts-b: Lease expired → take it → I am leaderThe leader died, so the renewals stopped. After the lease time, a standby takes over and sends the 08:00 email.
// posts/index.js (sketch) elector.on('leader', () => startNightlyEmailJob()); elector.on('follower', () => stopNightlyEmailJob());In code you only answer two questions: what to do when I become leader, and what to stop when I lose it. Go has a ready-made package for this (client-go's leaderelection); in other languages use a small library, or a sidecar that does the election for you.
Kubernetes uses this itself: only one controller manager and one scheduler are active at a time, even when three control-plane nodes run them (chapter 44).
| Outside (replicas: 1) | Inside (leader election) | |
|---|---|---|
| Code changes | None | Election logic or a library |
| Takeover time | A new Pod must start (seconds to minutes) | A standby is already running (about one lease period) |
| Good for | Most background jobs | When the gap must be short |
PodDisruptionBudget: keep enough running
Now the opposite problem. You have 3 posts Pods. An admin drains nodes to upgrade them. If all three posts Pods are evicted at once, your site is down. A PodDisruptionBudget (PDB) says how many may be missing at the same time because of planned work.
minAvailable
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: posts-pdb
spec:
minAvailable: 2
selector:
matchLabels: { app: posts }"Always keep at least 2 posts Pods running."
maxUnavailable
spec:
maxUnavailable: 1
selector:
matchLabels: { app: posts }"Never take away more than 1 at a time." Often better: it keeps working when you scale up or down. Percentages like "25%" also work.
| A PDB protects against | A PDB does not protect against |
|---|---|
Voluntary disruptions that go through the eviction API: kubectl drain, node upgrades, the cluster autoscaler removing a node | Involuntary ones: a node crashing, a power cut, a container running out of memory, you running kubectl delete pod |
Try it: drain a node
Three posts Pods, one per node. Pick a budget and drain a node. With minAvailable: 2, the drain waits until a replacement is Ready before evicting the next Pod. Then try minAvailable: 3.
minAvailable equal to the replica count (or maxUnavailable: 0) means "never evict anything". Node drains then hang forever and admins cannot upgrade the cluster. A PDB for a single-replica app has the same effect. Leave room for at least one disruption.$ kubectl get pdb
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
posts-pdb 2 N/A 1 3d
ALLOWED DISRUPTIONS is the number to watch: 0 means a drain will wait.
Which tool?
- A background worker that must never run twice, and a 20-second gap during deploys is fine
Simplest option, no code changes.
- A payment reconciler that must take over within seconds if it dies
Standbys are already running and grab the Lease quickly.
- Node upgrades keep taking the comments API down
A PDB makes drains evict comments Pods one at a time.
Recap
- Some work must be done by exactly one copy: scheduled emails, ordered processing, single-connection jobs.
- Outside: run one replica. With a Deployment use
strategy: Recreate, or use a StatefulSet for a strict "at most one". - Inside: run several copies and elect a leader with a Lease; standbys take over quickly.
- A PodDisruptionBudget limits how many Pods planned maintenance may take down at once.
- It does not help against crashes, and a too-strict PDB blocks node drains.
Quick check
replicas: 1, yet after one deploy some users got the email twice. What happened?Show answer
A rolling update started the new Pod before stopping the old one, so for a moment two copies ran, both at 08:00. Use strategy: Recreate, a StatefulSet, or a CronJob.
Singleton services and PodDisruptionBudgets: common questions
Can a Deployment with replicas 1 run two Pods?
Yes, briefly. A rolling update starts the new Pod before stopping the old one. Use strategy Recreate or a StatefulSet if two copies must never run together.
Does a PodDisruptionBudget protect against node crashes?
No. It only limits voluntary disruptions that use the eviction API, such as kubectl drain, node upgrades and the cluster autoscaler.