The control plane makes decisions. Worker nodes execute them. Every container you run lives on a worker node, managed by three components working in concert: the kubelet, kube-proxy, and the container runtime.

1. kubelet — The Node Agent

The kubelet is a systemd service (not a container) running on every worker node. It is the bridge between the Kubernetes API and the container runtime on that machine.

What the kubelet Does

  1. Watches the API server for Pods assigned to its node (via spec.nodeName)
  2. Translates Pod specs into container runtime calls (via CRI)
  3. Manages Pod lifecycle — starts, stops, restarts containers
  4. Runs probes — liveness, readiness, startup
  5. Reports status — Pod conditions and node conditions back to the API server
  6. Manages volumes — mounts/unmounts as Pods start/stop
  7. Enforces resources — sets cgroup limits for CPU/memory
  8. Evicts Pods — when node is under resource pressure
API Server (remote) kubelet Pod Lifecycle Mgr PLEG Volume Manager Eviction Manager Container Runtime containerd / CRI-O CNI Plugin CSI Driver (storage) Pods (containers) watch CRI gRPC CNI

PLEG — Pod Lifecycle Event Generator

The kubelet doesn't poll the container runtime constantly. Instead, PLEG periodically (every 1 second) queries the runtime for the list of all containers and compares it to the last known state. Changes generate events that trigger the kubelet's sync loop.

# A common production issue:
PLEG is not healthy
# This means PLEG took >3min to relist containers.
# Causes: overloaded node, slow container runtime, too many containers on one node.
"PLEG is not healthy" is one of the most common node NotReady causes. It typically means the node has too many Pods (container runtime is slow to list) or the runtime itself is stuck. Solution: check container runtime health, reduce Pod density, or investigate I/O pressure.

kubelet Pod Admission

Before starting a Pod, the kubelet runs its own admission checks (separate from API server admission):

  • Resource check: Does the node have enough allocatable CPU/memory?
  • PID pressure: Is the node running out of process IDs?
  • Node affinity: Double-check (in case of race conditions)
  • Tolerations: Can the Pod tolerate the node's taints?

If admission fails, the Pod stays Pending with a reason like OutOfCpu or OutOfMemory.

Node Registration

When the kubelet starts, it self-registers with the API server — creating or updating its Node object with:

  • Capacity (total CPU, memory, max pods)
  • Allocatable (capacity minus system-reserved and kube-reserved)
  • Node conditions (Ready, MemoryPressure, DiskPressure, PIDPressure)
  • Node labels and annotations
# Check allocatable vs capacity:
kubectl describe node worker-1 | grep -A5 "Allocatable"
# Allocatable:
#   cpu:     3800m       (4 cores - 200m reserved)
#   memory:  7400Mi      (8Gi - 600Mi reserved)
#   pods:    110         (default max)
Key: Allocatable ≠ Capacity. The kubelet reserves resources for system daemons (--system-reserved) and for itself (--kube-reserved). The scheduler uses allocatable when deciding if a Pod fits.

2. kube-proxy — Service Networking

kube-proxy makes Service objects work. When you create a Service with a ClusterIP, kube-proxy ensures that any traffic sent to that virtual IP gets routed to one of the backing Pods.

Three Modes

ModeMechanismWhen to Use
iptables (default)Programs iptables rules for NATMost clusters < 5000 Services
IPVSUses Linux IPVS kernel module (L4 load balancer)Large clusters, need advanced LB algorithms
nftables (new, v1.29+)Uses nftables instead of iptablesModern kernels, replacing iptables

How iptables Mode Works

For each Service, kube-proxy creates iptables rules that:

  1. DNAT packets destined for the ClusterIP to a randomly-selected Pod IP
  2. Apply probability-based load balancing (each Pod gets 1/N chance)
  3. Handle session affinity if configured
# Example: Service "web" (10.96.0.100:80) → 3 Pods
# kube-proxy generates rules like:
-A KUBE-SERVICES -d 10.96.0.100/32 -p tcp --dport 80 -j KUBE-SVC-XXXXX
-A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.333 -j KUBE-SEP-AAA
-A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.500 -j KUBE-SEP-BBB
-A KUBE-SVC-XXXXX -j KUBE-SEP-CCC
-A KUBE-SEP-AAA -p tcp -j DNAT --to-destination 10.244.1.5:8080
-A KUBE-SEP-BBB -p tcp -j DNAT --to-destination 10.244.2.3:8080
-A KUBE-SEP-CCC -p tcp -j DNAT --to-destination 10.244.3.7:8080
Key: kube-proxy doesn't proxy traffic itself (despite the name). It programs kernel-level rules. Traffic flows directly from source Pod → destination Pod via the kernel's networking stack. kube-proxy just configures the rules.

Why IPVS at Scale?

iptables rules are O(n) — every packet traverses the chain linearly. With 10,000 Services × 10 endpoints = 100,000 rules, this gets slow. IPVS uses hash tables (O(1) lookup) and supports multiple load-balancing algorithms: round-robin, least-connections, weighted, etc.

Signs you need IPVS: high latency on service calls, iptables-save output is hundreds of thousands of lines, kube-proxy takes minutes to sync rules after endpoint changes. Switch with: kube-proxy --proxy-mode=ipvs.

The "No kube-proxy" Option

Modern CNI plugins like Cilium can replace kube-proxy entirely using eBPF. Instead of iptables/IPVS rules, Cilium programs eBPF maps in the kernel — faster lookups, more features (topology-aware routing, Maglev hashing), and easier debugging.

3. Container Runtime & CRI

The kubelet doesn't run containers directly. It speaks the Container Runtime Interface (CRI) — a gRPC API — to a container runtime that does the actual work.

CRI Architecture

kubelet CRI gRPC containerd Image mgmt Snapshot mgmt Task lifecycle Networking (CNI) runc OCI runtime kata / gVisor sandboxed Linux container microVM / sandbox

CRI API — Two Services

ServiceMethodsPurpose
RuntimeServiceRunPodSandbox, CreateContainer, StartContainer, StopContainer, RemoveContainerPod & container lifecycle
ImageServicePullImage, ListImages, RemoveImage, ImageStatusImage management

containerd vs CRI-O

FeaturecontainerdCRI-O
OriginDocker (extracted as standalone)Red Hat / Kubernetes SIG
ScopeGeneral-purpose (also used outside K8s)Kubernetes-only
Used byEKS, GKE, Docker Desktop, k3sOpenShift, some kubeadm setups
OCI runtimerunc (default), kata, gVisorrunc (default), kata, gVisor
FootprintSlightly larger (more features)Minimal, K8s-focused
Docker removal (K8s 1.24): Kubernetes dropped dockershim. Docker was never a CRI-compliant runtime — the kubelet used a shim layer. Now it speaks CRI directly to containerd or CRI-O. Docker-built images still work perfectly (they're OCI images).

4. Pod Sandbox Lifecycle

A Pod is not just "a group of containers." At the OS level, a Pod is a sandbox — a set of shared Linux namespaces that containers join.

The Pause Container

Every Pod has a hidden "pause" container (also called the "infra container"). It's a tiny process that:

  1. Creates and holds the Pod's network namespace
  2. Acts as PID 1 (reaps zombie processes)
  3. Stays running for the Pod's entire lifetime

All other containers in the Pod join the pause container's namespaces. This is why containers in the same Pod share:

SharedImplication
Network namespaceSame IP, same ports, localhost communication
IPC namespaceShared memory, semaphores between containers
UTS namespace (optional)Same hostname
NOT shared: PID namespace (unless shareProcessNamespace: true)By default, each container has its own PID 1
NOT shared: FilesystemMust use emptyDir volumes to share files

Full Pod Startup Sequence

1. RunPodSandbox (pause ctr) 2. CNI: attach network interface 3. Pull images (if not cached) 4. Run init containers (sequential) 5. Run app containers (parallel) 6. Start probes (startup first) Creates network ns, IPC ns, mounts Assigns Pod IP, sets up veth pair ImagePullBackOff if this fails Each must exit 0 before next starts All app containers start together Pod becomes Ready when readiness passes

Pod Shutdown Sequence

When a Pod is deleted, the kubelet executes a graceful shutdown:

  1. Pod enters Terminating state
  2. Pod is removed from Service endpoints (stops receiving traffic)
  3. preStop hook runs (if defined)
  4. SIGTERM sent to PID 1 in each container
  5. Wait up to terminationGracePeriodSeconds (default: 30s)
  6. If still running: SIGKILL
  7. CNI: detach network
  8. Remove sandbox
A common bug: your app doesn't handle SIGTERM, and gets SIGKILL'd after 30s. Users see connection resets during deploys. Fix: handle SIGTERM in your app (graceful drain), or use a preStop hook: lifecycle.preStop.exec.command: ["sleep", "5"] to give in-flight requests time to complete.
Race condition: Endpoint removal (step 2) and SIGTERM (step 4) happen concurrently, not sequentially. Traffic might still arrive after SIGTERM is sent because kube-proxy hasn't updated its rules yet. This is why the preStop: sleep 5 pattern exists — it gives time for endpoint propagation.

5. Eviction Manager — When the Node Runs Out

The kubelet monitors node resources and evicts Pods when thresholds are breached:

SignalDefault ThresholdCondition Set
memory.available< 100MiMemoryPressure
nodefs.available< 10%DiskPressure
imagefs.available< 15%DiskPressure
pid.available< variesPIDPressure

Eviction Order (QoS-based)

  1. BestEffort Pods (no requests/limits) — evicted first
  2. Burstable Pods exceeding their requests
  3. Guaranteed Pods (requests = limits) — evicted last, only in extreme pressure
Always set resource requests on production workloads. Without them, your Pods are BestEffort and are the first to be killed under memory pressure — regardless of how important they are. Resource requests are not just for scheduling — they're your eviction shield.

Node Status — What the Control Plane Sees

kubectl describe node worker-1

Conditions:
  Type             Status  Reason
  ----             ------  ------
  Ready            True    KubeletReady
  MemoryPressure   False   KubeletHasSufficientMemory
  DiskPressure     False   KubeletHasNoDiskPressure
  PIDPressure      False   KubeletHasSufficientPID

# If Ready becomes Unknown (kubelet stops reporting):
# → Node controller waits 40s, then marks NotReady
# → After pod-eviction-timeout (5min), Pods are evicted

Summary

ComponentRuns AsPrimary JobFailure Impact
kubeletsystemd servicePod lifecycle, reportingNode goes NotReady, no new Pods
kube-proxyDaemonSet (usually)Service → Pod routing rulesServices stop working on that node
containerdsystemd servicePull images, run containersNo containers start, PLEG fails
pause containerPer-Pod infra containerHold namespacesPod's network dies

📝 Quiz: Worker Node Internals

Q1: What is the pause container and why does every Pod have one?

The pause container is a minimal infra container that creates and holds the Pod's network namespace. Other containers join its namespaces. It ensures the network namespace persists even if app containers restart, and it serves as PID 1 to reap zombie processes.

Q2: A Pod has containers A and B. Container A listens on port 8080. How does container B reach it?

localhost:8080. Since both containers share the same network namespace (via the pause container), they share the same network stack and can communicate over localhost.

Q3: You see "PLEG is not healthy" in kubelet logs. What does this mean and what do you check?

PLEG (Pod Lifecycle Event Generator) took too long to list containers from the runtime (>3 min). Check: (1) Container runtime health (crictl ps). (2) Node I/O and CPU pressure. (3) Number of Pods/containers on that node — may be too many.

Q4: During a rolling deployment, users see brief connection resets. What's likely happening and how do you fix it?

Race condition: SIGTERM is sent to the Pod concurrently with endpoint removal. Traffic arrives after the Pod starts shutting down but before kube-proxy removes it from routing rules. Fix: add a preStop: sleep 5 hook to delay shutdown, giving endpoint propagation time to complete.

Q5: Why does Kubernetes use CRI (an abstraction) instead of calling containerd directly?

Pluggability. CRI is a standard gRPC interface that any runtime can implement. This lets you swap containerd for CRI-O, or use specialized runtimes (gVisor, Kata) for different security/isolation needs — without changing the kubelet at all.

Q6: A node has 4 CPU cores. --kube-reserved=500m and --system-reserved=500m are set. How much CPU is allocatable for Pods?

3000m (3 cores). Allocatable = Capacity - kube-reserved - system-reserved = 4000m - 500m - 500m = 3000m. The scheduler will never assign Pods requesting more than 3000m total CPU to this node.