ReplicaSets

Keeping N identical pods alive with a selector.

Beginner10 min · lesson 8 of 65
In plain terms
A ReplicaSet is a manager told “always keep three cashiers on the floor.” Someone clocks out, they call in a replacement; too many showed up, they send one home. The number is the whole job.

Delete a running Pod and it comes back within seconds. Kubernetes wasn't watching that one specific Pod, waiting to resurrect it. Something counted the Pods, noticed the total had dropped below what you asked for, and stamped out a fresh one to fill the gap. That something is usually a ReplicaSet. A Pod is Kubernetes' smallest unit of work, one or more containers that start and stop together, and a ReplicaSet is a small controller with exactly one job: keep N copies of a Pod running, no more and no fewer.

A ReplicaSet has no list of which Pods belong to it. It goes by a badge instead: Pods carrying the right label count toward the total, and Pods without it are none of its business. That badge check is called the selector, and it is the machinery underneath everything else in this lesson. You'll rarely create a ReplicaSet by hand, because a Deployment builds them for you, but this is the object where the 'keep N alive' guarantee physically lives. Worth understanding on its own.

The loop, and the three fields that feed it

A ReplicaSet is defined by three fields. replicas is the number of Pods you want. template is the shape of each Pod it should create, the same spec you'd write for a standalone Pod. selector is a label query, a filter like app=web, that tells the ReplicaSet which Pods in the namespace count as its own. Behind these three fields runs a loop that never really stops, and it behaves like a thermostat. A thermostat reads the room temperature, compares it to the number you dialed in, switches the heat on or off, then reads again a moment later. The ReplicaSet controller reads how many Pods currently match its selector, compares that to replicas, creates or deletes Pods to close the gap, then reads again. That loop is why a crashed Pod comes back on its own. The live count fell below desired, so the next pass builds a replacement. Nobody wrote a recovery script. Self-healing falls out of the counting.

That loop lives inside kube-controller-manager, the control-plane process that runs most of Kubernetes' built-in controllers. Think of the control plane as the cluster's brain, the part that makes decisions rather than doing the work. So how does the loop notice a dead Pod so fast? Not by asking over and over. It works like a mailroom that gets a buzz the moment a package lands, instead of walking down to the loading dock every minute to check. The controller keeps an open subscription to Pod and ReplicaSet changes through the API server (the one component everything in the cluster talks to), so a Pod deletion pings the loop almost the instant it happens. That's why the replacement for a deleted or crashed Pod feels immediate instead of showing up on some timer. A failed node is the one case that really is on a timer. The control plane waits the better part of a minute of silence before it marks the node NotReady, and the default not-ready and unreachable tolerations then hold that node's Pods in place for a further five minutes before they are evicted. Only at that point does the matching count drop and the ReplicaSet build replacements on other nodes, so Pods sitting on a dead node for five-plus minutes is normal, not a broken cluster.

web-rs.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web # MUST match the selector above
spec:
containers:
- name: app
image: nginx:1.27
ports:
- containerPort: 80
apply it, then read the count
kubectl apply -f web-rs.yaml
kubectl get rs web
output
replicaset.apps/web created
NAME DESIRED CURRENT READY AGE
web 3 3 3 12s

DESIRED is your replicas, the target. CURRENT is how many the controller has actually created. READY is how many of those pass their health checks and can serve traffic. When all three line up, the loop is at rest and does nothing. So break it on purpose and watch it heal.

kill one Pod, watch the loop replace it
kubectl get pods -l app=web
kubectl delete pod web-7v2kd # use a name from the list above
kubectl get pods -l app=web
output
NAME READY STATUS RESTARTS AGE
web-7v2kd 1/1 Running 0 40s
web-9xlpm 1/1 Running 0 40s
web-q4t8n 1/1 Running 0 40s
pod "web-7v2kd" deleted
NAME READY STATUS RESTARTS AGE
web-9xlpm 1/1 Running 0 55s
web-q4t8n 1/1 Running 0 55s
web-pm6rd 0/1 ContainerCreating 0 2s

The deleted Pod is gone, and a brand new one, web-pm6rd, is already on its way up. It carries a different random suffix because it's a genuinely new Pod, not the old one revived. It appeared the instant the matching count dropped to two.

The selector is an ownership contract

The selector does more than filter what you see. It draws the ownership boundary. A ReplicaSet owns exactly the Pods whose labels match its selector, and Kubernetes records that ownership by stamping every Pod the ReplicaSet creates with an ownerReference pointing back at it. Two behaviors fall out of this, and both catch people off guard. First, adoption: if a loose Pod with matching labels already exists, the ReplicaSet counts it toward replicas instead of making a new one. Second, orphaning: relabel a running Pod so it no longer matches, and the ReplicaSet loses sight of it, its owned count drops, and it builds a replacement while the relabeled Pod keeps running with nothing managing it. You can read that ownership link straight off any Pod.

which controller owns this Pod?
kubectl get pod web-9xlpm -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
output
ReplicaSet/web

Why you almost never write one by hand

A ReplicaSet keeps Pods alive and does nothing else. It has no concept of a version. Change the image in its template and the running Pods don't budge. Only Pods created after the edit get the new image, so rolling out an update would mean deleting Pods by hand to force fresh ones. That gap is exactly what a Deployment fills. A Deployment is a higher-level object that creates and steers ReplicaSets for you, adding rolling updates and one-command rollback (that's the next lesson). Look under any Deployment and you'll find a ReplicaSet with a hash in its name.

a Deployment is ReplicaSets underneath
kubectl get rs -l app=web
output
NAME DESIRED CURRENT READY AGE
web-5c9f7b8d64 3 3 3 6m
web-7d4f9c6b55 0 0 0 21m

The suffix like 5c9f7b8d64 is a pod-template-hash, a fingerprint the Deployment adds so each version of the template gets its own ReplicaSet. The one parked at zero is the previous version, kept around empty so a rollback is instant. During a live rollout you'd catch both moving at once, the new one climbing toward three and the old one draining toward zero.

One reconcile pass, one decision
Reconcile loop wakes
count Pods matching selector app=web
count < replicas
create Pods from the template
crashed Pod or dead node dropped the count; replacement appears
count > replicas
delete surplus Pods
scaled down, or a stray matching Pod got adopted
count == replicas
do nothing, wait for the next event
steady state; the loop sleeps until something changes
The controller only ever compares the observed count to the desired count, then closes the gap. That single comparison is the whole engine. Self-healing after a crash, scaling to a new number, replacing a Pod you accidentally orphaned: it's all the same loop noticing a gap and filling it.
Editing a ReplicaSet that a Deployment owns gets silently reverted
If a Deployment created the ReplicaSet, treat that ReplicaSet as read-only. Run kubectl scale rs/web-5c9f7b8d64 --replicas=5 on it and the change works for a few seconds, then snaps back to three. The Deployment runs its own reconcile loop, it sees a ReplicaSet that disagrees with its spec, and it corrects the ReplicaSet right back. People burn real time here, staring at a change that 'won't take.' Make the change on the Deployment instead (kubectl scale deployment/web --replicas=5) and it flows down to the ReplicaSet and stays.

If the labels in template don't match spec.selector, you never get as far as stray Pods: the API server rejects the object on apply, with selector does not match template labels. Orphaning is a separate thing, and it comes from relabeling a Pod that is already running.

Scaling a ReplicaSet is blunt. Prefer Deployment for user-facing apps so revisions stay tracked.

The hash in a Deployment-managed ReplicaSet name is how rollouts keep old and new sets distinct.

Try this

Apply a ReplicaSet with three replicas, delete one Pod, and confirm the count comes back. Then read the ownerReference off a surviving Pod to see what is holding that count at three. Pod name suffixes are random, so these commands read a name out of the cluster instead of hardcoding one.

terminal
kubectl apply -f web-rs.yaml
kubectl get rs web
# grab whichever Pod name your cluster generated, then delete that one
POD=$(kubectl get pods -l app=web -o jsonpath='{.items[0].metadata.name}')
kubectl delete pod "$POD"
kubectl get pods -l app=web
# ask a Pod that is still there who owns it
POD=$(kubectl get pods -l app=web -o jsonpath='{.items[0].metadata.name}')
kubectl get pod "$POD" -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
kubectl get rs -l app=web

Takeaway

A ReplicaSet keeps N identical pods alive by label selector. Deployments own ReplicaSets so you get rollouts; raw ReplicaSets are the mechanism underneath.

Quick check
01You change the app label on one running Pod from web to web-debug so you can poke at it in isolation. The ReplicaSet still has replicas: 3. What happens?
Correct — Relabeling drops the count of Pods matching the selector to 2, so the loop creates one replacement to get back to 3. The orphaned Pod keeps running with no owner, so you end up with 4.
Incorrect — A ReplicaSet only ever deletes Pods it owns, meaning ones that match its selector. The relabeled Pod no longer matches, so the ReplicaSet won't touch it.
Incorrect — The count of Pods matching app=web just dropped to 2. The loop is not satisfied and will act on the next pass.
Incorrect — Controllers never rewrite a Pod's labels to force a match. With no matching label there is no adoption, and the Pod stays orphaned.
02A ReplicaSet has replicas: 3 running nginx:1.24. You edit the ReplicaSet's pod template to nginx:1.27 and save. What happens to the three pods already running?
Incorrect — A ReplicaSet has no rollout logic; staged replacement is a Deployment's job, not a ReplicaSet's.
Correct — a ReplicaSet only counts pods and has no concept of a version, so it never disturbs pods that already satisfy the count. That gap is precisely why Deployments exist.
Incorrect — The count of matching pods is already 3, so the loop is satisfied and deletes nothing.
Incorrect — A template edit is accepted silently; a template that differs from running pods is not an error condition.
03A ReplicaSet named web-5c9f7b8d64 was created by a Deployment. You run kubectl scale rs/web-5c9f7b8d64 --replicas=5. It shows 5 for a few seconds, then snaps back to 3. Why, and what should you do?
Incorrect — Capacity shortfalls leave pods Pending, they do not revert the count to exactly 3; the clean revert points to a controller, not the scheduler.
Incorrect — The command succeeded and the revert is deliberate; --record only annotates history and cannot make a change persist.
Correct — a Deployment continuously enforces its spec onto the ReplicaSets it owns, so kubectl scale deployment/web --replicas=5 is the change that flows down and stays.
Incorrect — A standalone ReplicaSet scales by hand just fine; the revert here is specifically the parent Deployment overriding the edit.

Related