Back to Blog

How Kubernetes Services Actually Route Traffic for the CKA Exam: kube-proxy Modes (iptables, IPVS, nftables), EndpointSlices, Traffic Policies & Session Affinity

A hands-on CKA guide to what happens after a packet hits a ClusterIP — how kube-proxy programs iptables, IPVS or nftables rules from EndpointSlices, what externalTrafficPolicy and internalTrafficPolicy really change, how trafficDistribution PreferSameZone and PreferSameNode work, when sessionAffinity ClientIP is the right answer, and how to debug a Service that has endpoints but still drops traffic.

By Sailor Team , October 12, 2026

Every CKA candidate can create a Service. Most can explain that a ClusterIP is “virtual” and that kube-proxy “makes it work.” Far fewer can say what kube-proxy actually writes into the kernel, why a Service with healthy endpoints can still drop packets on one node, or why a LoadBalancer Service suddenly started failing health checks after someone set externalTrafficPolicy: Local.

That gap matters on the exam. The Services & Networking domain is 20% of the CKA, and its objectives include understanding connectivity between Pods and using ClusterIP, NodePort and LoadBalancer Services and endpoints. The scored tasks are practical — fix the Service, make the app reachable, preserve the client IP — but you solve them faster when you know the mechanism underneath. The CKA networking deep dive covers Service types, DNS, Ingress and NetworkPolicy; this article goes one layer down to the dataplane: how a Service’s virtual IP is turned into real forwarding rules, and every knob that changes which backend a packet reaches.

The Four-Step Chain Behind Every Service

Before touching any flags, fix this sequence in your head. It is the same for ClusterIP, NodePort and LoadBalancer:

  1. You create a Service with a selector. The API server allocates a ClusterIP from the --service-cluster-ip-range and, for NodePort and LoadBalancer types, a node port from --service-node-port-range (default 30000-32767).
  2. The EndpointSlice controller (in kube-controller-manager) watches Pods matching the selector and writes their IPs, ports and readiness into EndpointSlice objects labelled kubernetes.io/service-name=<svc>.
  3. kube-proxy on every node watches Services and EndpointSlices and programs the node’s packet-forwarding rules — iptables, IPVS or nftables on Linux — so a packet to ClusterIP:port is rewritten to PodIP:targetPort.
  4. The CNI plugin carries the rewritten packet to the Pod, wherever it lives.

Nothing in that chain “proxies” traffic through a userspace process. kube-proxy is a controller that writes kernel rules; the kernel does the forwarding. That is why a dead kube-proxy does not break existing Services on a node — the rules stay in place — but stops new Services and endpoint changes from being reflected. It is a classic exam trap worth remembering.

EndpointSlices: The Source of Truth for Backends

The CKA no longer treats the legacy Endpoints object as primary. The Endpoints API has been deprecated since Kubernetes v1.33; EndpointSlice (stable since v1.21) is what kube-proxy, CoreDNS and modern load balancers read.

kubectl get endpointslices -l kubernetes.io/service-name=web -o wide
kubectl get endpointslice -l kubernetes.io/service-name=web -o yaml

A slice looks like this (trimmed):

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: web-7xk2p
  labels:
    kubernetes.io/service-name: web
    endpointslice.kubernetes.io/managed-by: endpointslice-controller.k8s.io
addressType: IPv4
ports:
  - name: http
    port: 8080
    protocol: TCP
endpoints:
  - addresses: ["10.244.1.17"]
    nodeName: worker-1
    zone: eu-west-1a
    conditions:
      ready: true
      serving: true
      terminating: false

Facts the exam can reasonably expect:

DetailValueWhy it matters
Default endpoints per slice100 (--max-endpoints-per-slice, max 1000)Large Services span several slices; always filter by the service-name label
Legacy Endpoints cap1000 endpoints, then truncated with endpoints.kubernetes.io/over-capacity: truncatedOnly affects consumers still reading Endpoints
ready conditionPod is Ready and not terminatingThe default set kube-proxy forwards to
serving conditionPod is Ready, regardless of deletionUsed when all endpoints are terminating
terminating conditionPod has a deletion timestampSet the moment kubectl delete pod runs, before containers exit
publishNotReadyAddresses: trueMarks every endpoint readyNeeded by StatefulSet peer discovery before Pods pass readiness

The serving/terminating split is the mechanism behind graceful rolling updates. When a Pod is deleted it is removed from the ready set immediately, so new connections stop arriving while the container’s preStop hook and terminationGracePeriodSeconds play out — the sequence covered in the Pod lifecycle and graceful shutdown guide. If every endpoint for a Service is terminating, kube-proxy will fall back to the ones still serving rather than drop traffic.

Exam reflex: a Service that “has no endpoints” is almost always a selector/label mismatch or a targetPort that does not match the container port. Check kubectl get endpointslices -l kubernetes.io/service-name=<svc> before you touch kube-proxy.

kube-proxy Modes: iptables, IPVS and nftables

kube-proxy has one job but three Linux backends. The mode is set with --proxy-mode or, in kubeadm clusters, the mode: field of the KubeProxyConfiguration stored in the kube-proxy ConfigMap in kube-system.

# kubeadm clusters: inspect the effective config
kubectl -n kube-system get cm kube-proxy -o yaml | grep -A1 -E '^\s*mode:'

# ask a running kube-proxy which mode it chose (metrics port 10249)
kubectl -n kube-system exec ds/kube-proxy -- \
  wget -qO- http://127.0.0.1:10249/proxyMode

# or read the first lines of its log
kubectl -n kube-system logs ds/kube-proxy | grep -i -E 'proxier|proxy mode'

iptables mode (today’s default)

kube-proxy installs chains in the nat table: KUBE-SERVICES matches on ClusterIP and port, jumps to a per-Service KUBE-SVC-* chain, which uses the statistic match to pick one KUBE-SEP-* (endpoint) chain at random, which performs the DNAT. NodePorts are matched in KUBE-NODEPORTS; traffic that needs SNAT is tagged via KUBE-MARK-MASQ.

# on a node: follow one Service through the rules
sudo iptables-save -t nat | grep -E 'KUBE-SVC|KUBE-SEP' | grep web

Two tuning parameters show up in configuration questions:

  • iptables.minSyncPeriod (default 1s): the minimum gap between rule re-syncs so that bursts of endpoint changes are batched. Since v1.28 kube-proxy only rewrites the chains that changed, so the old advice to raise this to many seconds is obsolete.
  • iptables.syncPeriod (default 30s): how often kube-proxy does a full reconcile to repair rules something else deleted. Setting it very large is no longer recommended.

The weakness of iptables mode is linear rule evaluation: with tens of thousands of Services, every packet walks a long chain and every update rewrites a large ruleset.

IPVS mode (deprecated)

IPVS (IP Virtual Server) is a kernel load balancer with hash-table lookups, so it scales better than iptables and offers real scheduling algorithms:

ipvs.schedulerBehaviour
rrRound robin (default)
wrrWeighted round robin
lc / wlcLeast connections / weighted least connections
shSource hashing — a crude form of client stickiness
lblc, lblcr, sed, nqLocality and shortest-expected-delay variants
sudo ipvsadm -Ln          # one virtual server per Service IP:port, real servers = endpoints

The current documentation is blunt about its future: the ipvs mode is deprecated as of v1.35, will be disabled by default in v1.40 (re-enable with the KubeProxyIPVS feature gate) and removed in v1.43. The kernel IPVS API turned out to be a poor fit for the Service model, and nftables delivers the same performance goals with a cleaner implementation. If an exam question or interviewer asks “which mode for large clusters?”, the forward-looking answer is nftables, not IPVS.

nftables mode (the successor)

The nftables backend (NFTablesProxyMode) went alpha in v1.29, beta in v1.31 and GA in v1.33. It requires kernel 5.13 or newer, processes endpoint changes faster than iptables and uses nftables maps and verdict maps so lookup cost does not grow linearly with the number of Services. In v1.37 the default is still iptables, but the documentation states plainly that a future release will flip the default to nftables — and recommends every cluster set mode explicitly so an upgrade never changes the backend by surprise.

# KubeProxyConfiguration excerpt
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: nftables
nftables:
  minSyncPeriod: 1s
sudo nft list table ip kube-proxy | head -40

Behavioural differences you should know before migrating a cluster from iptables to nftables:

  • NodePort reachability: iptables mode binds NodePorts on every local IP; nftables defaults to --nodeport-addresses primary, so NodePorts answer only on the node’s primary IPv4/IPv6 address. Override with nodePortAddresses: ["0.0.0.0/0"] if you depend on the old behaviour.
  • Firewall rules: iptables mode added accept rules for NodePorts to coexist with aggressive host firewalls; nftables mode adds nothing, so you must open the NodePort range yourself.
  • Conntrack workaround: iptables mode installs a workaround for a pre-6.1 kernel bug that reset long-lived TCP connections; nftables does not. Check the iptables_ct_state_invalid_dropped_packets_total metric and, if needed, run with --conntrack-tcp-be-liberal.
  • Localhost NodePorts (127.0.0.1:3xxxx) are off in nftables mode; a KubeProxyNFTablesLocalhostNodePorts gate (alpha in v1.37) restores them.

Mode comparison for revision

iptablesIPVSnftables
Status (v1.37)Default, stableDeprecated since 1.35; removal 1.43Stable since 1.33; future default
Kernel requirementAny modern kernelip_vs modules5.13+
Backend selectionRandom (statistic match)Configurable schedulerRandom
ScalingLinear rule walkHash lookupMap lookup
Inspectiptables-save -t natipvsadm -Lnnft list table ip kube-proxy

Some CNI plugins (Cilium is the common example) can replace kube-proxy entirely with eBPF programs. The CKA does not require configuring that, but if kubectl -n kube-system get ds kube-proxy returns nothing on a cluster, do not assume something is broken — check which CNI is installed, as described in the CNI awareness section of the networking deep dive.

Traffic Policies: Cluster vs Local

Two Service fields decide which nodes’ endpoints a packet may be forwarded to. They are independent and apply to different traffic sources.

externalTrafficPolicy (NodePort and LoadBalancer)

Cluster (default)Local
Eligible backendsAll ready endpoints in the clusterOnly ready endpoints on the receiving node
Second network hopYes, when the backend is on another nodeNever
Client source IPLost — kube-proxy SNATs to the node IPPreserved
Load distributionEven across PodsEven across nodes, so uneven across Pods
Node with no local PodsStill forwards (to another node)Drops the traffic
LoadBalancer health checkNODE_IP:10256/healthz from kube-proxyhealthCheckNodePort, returns 200 only when a local endpoint exists

Why the source IP is lost in Cluster mode: if node A receives a packet and forwards it to a Pod on node B, the reply must come back through node A for the connection to make sense. kube-proxy guarantees that by rewriting the source to node A’s IP. In Local mode the Pod is on the same node, so no rewrite is needed and the application sees the real client address — which is the whole reason externalTrafficPolicy: Local exists.

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: LoadBalancer
  selector:
    app: web
  externalTrafficPolicy: Local
  ports:
    - port: 80
      targetPort: 8080
kubectl get svc web -o jsonpath='{.spec.healthCheckNodePort}{"\n"}'

For a LoadBalancer Service with Local policy, Kubernetes allocates a healthCheckNodePort from the NodePort range. The cloud load balancer probes it on every node; kube-proxy answers 200 only on nodes that currently host a ready endpoint, so the load balancer stops sending traffic to nodes where it would be dropped. In Cluster mode the load balancer instead probes kube-proxy’s own health endpoint on port 10256, which returns 503 while a node is being deleted so connections can drain.

Since v1.28 (ProxyTerminatingEndpoints, GA), Local policy has one extra rule: if a node’s only local endpoints are all terminating, kube-proxy keeps forwarding to them instead of dropping. That bridges the gap during a rolling update between the last local Pod being deleted and the load balancer noticing the failed health check.

Exam scenario: “The application logs show every request coming from a 10.x node address. Make the real client IP visible.” Answer: set externalTrafficPolicy: Local on the NodePort/LoadBalancer Service. Then be ready to explain the trade-off — nodes without a replica stop serving, so pair it with a DaemonSet or with Pod topology spread constraints to keep a replica on each node.

internalTrafficPolicy (ClusterIP traffic)

Stable since v1.26, internalTrafficPolicy: Local tells kube-proxy to route in-cluster traffic to a Service only to endpoints on the same node as the client Pod. If the node has no local endpoint, the traffic is dropped — there is no fallback.

spec:
  internalTrafficPolicy: Local

Use it for node-local agents exposed as a Service — a per-node log shipper, metrics sidecar or DNS cache deployed as a DaemonSet — where you want “talk to the copy on my node” semantics with a stable name. Do not use it for ordinary Deployments: a client on a node without a replica simply gets connection failures.

trafficDistribution: Preferring Nearby Endpoints Without Hard Failures

Traffic policies are binary — all endpoints or only local ones. spec.trafficDistribution (ServiceTrafficDistribution, GA in v1.33) adds a preference with graceful fallback, implemented through hints that the EndpointSlice controller writes and kube-proxy honours.

ValueBehaviourStatus
PreferSameZoneUse endpoints in the client’s zone; if the zone has none, fall back cluster-wideGA (v1.35, via PreferSameTrafficDistribution)
PreferSameNodeUse endpoints on the client’s node; fall back to same zone, then cluster-wideGA (v1.35)
PreferCloseOlder alias for PreferSameZoneDeprecated
apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    app: api
  trafficDistribution: PreferSameZone
  ports:
    - port: 80
      targetPort: 8080

Zone comes from the topology.kubernetes.io/zone label on the node that runs each endpoint Pod; the controller copies it into endpoints[].zone and writes hints.forZones on the slice.

How this differs from the older Topology Aware Routing annotation (service.kubernetes.io/topology-mode: Auto, GA in v1.33):

  • Auto mode tries to allocate endpoints to zones proportionally to allocatable CPU and applies safeguards — it needs at least roughly three endpoints per zone, every node must have a zone label, and it silently falls back to cluster-wide routing when balance is impossible. Predictable it is not.
  • PreferSameZone is deliberately simple: if a zone has endpoints, they get all that zone’s traffic. You own the risk of overloading a small zone.
  • If both are set, the annotation wins; the annotation is expected to be deprecated in favour of the field.
  • A Local traffic policy takes precedence over trafficDistribution for that traffic direction; the field only guides routing when the corresponding policy is Cluster.

For the exam, the decision table is short: need the client IP → externalTrafficPolicy: Local; need node-local only, failures acceptable → internalTrafficPolicy: Local; want lower cross-zone latency and egress cost but never a hard failure → trafficDistribution: PreferSameZone.

Session Affinity: Sticky Clients

By default each new connection to a Service is load-balanced independently. Setting sessionAffinity: ClientIP makes kube-proxy pin all connections from one client IP to the same endpoint for a window:

spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800   # default: 3 hours; max 86400

Two cautions that turn into trick questions. First, affinity is keyed on the source IP kube-proxy sees — with externalTrafficPolicy: Cluster on a NodePort, that may be a node IP after SNAT, so every external client collapses into one “session”. Second, affinity is per connection tuple in the kernel, so a Pod restart breaks stickiness anyway. Applications that need real sessions should externalise state; ClientIP affinity is a stopgap, not a design.

Masquerading, Conntrack and the Limits That Bite

A few kernel-side details explain otherwise mysterious failures:

  • SNAT/masquerade. kube-proxy masquerades traffic that leaves the Pod network (NodePort Cluster mode, hairpin traffic from a Pod to its own Service) using KUBE-MARK-MASQ. --masquerade-all forces SNAT for all Service traffic and is sometimes required by CNIs that cannot route Pod IPs symmetrically.
  • Hairpin traffic. A Pod connecting to a Service that load-balances back to itself needs the kubelet’s hairpin-mode (hairpin-veth or promiscuous-bridge) when using a bridge CNI. The official debug-service guide lists this as the cause when a Pod can reach every replica except itself.
  • Conntrack sizing. kube-proxy sets the connection-tracking table from --conntrack-max-per-core (default 32768) with a floor of --conntrack-min (131072), and TCP established entries expire after --conntrack-tcp-timeout-established (default 24h). “Table full, dropping packet” in dmesg on a busy ingress node means these are too small.
  • UDP and DNS. Conntrack entries for UDP outlive Pod churn; a stale entry can keep pointing DNS traffic at a dead CoreDNS Pod. The symptoms and the ndots interaction are covered in the CoreDNS troubleshooting guide.

A Debugging Sequence for “Service Has Endpoints but Traffic Fails”

The official debug-service checklist is long; this is the condensed order that works under exam time pressure.

# 1. Does the Service resolve and have the ports you think?
kubectl -n app get svc web -o wide
kubectl -n app run tmp --rm -it --image=busybox:1.36 --restart=Never -- \
  nslookup web.app.svc.cluster.local

# 2. Does it have READY endpoints? (selector, targetPort, readiness)
kubectl -n app get endpointslices -l kubernetes.io/service-name=web -o wide
kubectl -n app get pods -l app=web -o wide --show-labels

# 3. Do the Pods answer directly? (bypasses kube-proxy)
kubectl -n app exec deploy/client -- wget -qO- --timeout=2 http://10.244.1.17:8080/

# 4. Is kube-proxy healthy on the node where the client runs?
kubectl -n kube-system get pods -l k8s-app=kube-proxy -o wide
kubectl -n kube-system logs <kube-proxy-pod> --tail=50
curl -s http://<node-ip>:10256/healthz     # 200 = programming rules fine

# 5. Are the rules actually there?
sudo iptables-save -t nat | grep -c KUBE-SVC        # iptables mode
sudo nft list table ip kube-proxy | grep -c 'service'  # nftables mode

# 6. Is a traffic policy excluding this node?
kubectl -n app get svc web -o jsonpath='{.spec.internalTrafficPolicy} {.spec.externalTrafficPolicy}{"\n"}'

# 7. Is a NetworkPolicy, not the Service, blocking it?
kubectl -n app get networkpolicy

Step 6 is the one people skip. A Service with internalTrafficPolicy: Local works perfectly from Pods on nodes that host a replica and times out from everywhere else — which looks exactly like a flaky network until you read the spec. Step 7 hands off to the NetworkPolicy guide; a default-deny policy in the namespace produces identical symptoms.

Common Mistakes on This Topic

  1. Restarting kube-proxy to “fix” a Service with zero endpoints. kube-proxy only programs what EndpointSlices contain; fix the selector or readiness probe.
  2. Setting externalTrafficPolicy: Local on a Deployment with two replicas across ten nodes. Eight nodes now drop traffic. Check healthCheckNodePort behaviour and spread replicas.
  3. Expecting internalTrafficPolicy: Local to fall back. It does not. Use trafficDistribution: PreferSameNode when you want preference with fallback.
  4. Assuming sessionAffinity: ClientIP sees real client IPs behind a NodePort. Not with externalTrafficPolicy: Cluster.
  5. Choosing IPVS for a new cluster “because it scales.” It is deprecated; nftables is the supported path, with explicit mode: set in the config.
  6. Reading Endpoints instead of EndpointSlices. The old object truncates at 1000 and is deprecated since v1.33.
  7. Forgetting that nftables mode changes NodePort binding to the primary node IP only.
  8. Debugging on the wrong node. kube-proxy state is per node; test from a Pod on the node that is failing.

How to Practise This

Build a two-node kubeadm cluster (the lab setup guide walks through it) and run these experiments — each takes under ten minutes and maps to a likely task:

  • Deploy a 2-replica app with a NodePort Service, hit the NodePort on a node without a replica, then set externalTrafficPolicy: Local and watch it stop answering. Read healthCheckNodePort.
  • Switch kube-proxy to nftables by editing the kube-proxy ConfigMap and restarting the DaemonSet; confirm with the /proxyMode endpoint and nft list table ip kube-proxy. Note what happened to NodePorts on secondary IPs.
  • Label your nodes with topology.kubernetes.io/zone, set trafficDistribution: PreferSameZone, and inspect hints.forZones on the EndpointSlice.
  • Delete a backend Pod while running a request loop; watch terminating: true appear on the slice before the Pod disappears.

Once the mechanics feel automatic, the remaining risk is speed under exam conditions. Sailor.sh’s Certified Kubernetes Administrator (CKA) Mock Exam Bundle puts Service and kube-proxy troubleshooting tasks inside a real multi-node cluster with the same remote-desktop constraints as the live exam, so you find out whether your debugging sequence holds up at minute 95 — and the free CKA practice questions are a good place to start before committing to a full timed run.

Frequently Asked Questions

What does kube-proxy actually do?

It watches Services and EndpointSlices and programs kernel packet-forwarding rules (iptables, IPVS or nftables on Linux) so that traffic to a Service’s virtual IP is rewritten to a backend Pod IP. It does not forward packets itself; the kernel does.

Which kube-proxy mode should I use in 2026?

Set the mode explicitly. iptables is still the default in v1.37 and works everywhere; nftables has been GA since v1.33, scales better and is slated to become the default. IPVS is deprecated since v1.35 and scheduled for removal in v1.43, so avoid it for new clusters.

What is the difference between externalTrafficPolicy Local and Cluster?

Cluster sends external traffic to any ready endpoint, may add a second hop and replaces the client source IP with the node IP. Local only uses endpoints on the receiving node, preserves the client IP, and drops traffic on nodes with no local endpoint; LoadBalancer Services get a healthCheckNodePort so the cloud load balancer avoids those nodes.

When should I set internalTrafficPolicy to Local?

When clients should talk only to the replica on their own node — typically a DaemonSet-backed agent — and a hard failure is acceptable if no local replica exists. For a soft preference, use trafficDistribution: PreferSameNode instead.

What is trafficDistribution PreferSameZone and how is it different from topology-aware hints?

PreferSameZone routes to endpoints in the client’s zone when any exist and otherwise falls back cluster-wide, with no safeguards. The topology-mode: Auto annotation allocates endpoints proportionally to zone CPU and refuses to apply hints when balance is impossible. PreferClose is a deprecated alias for PreferSameZone; PreferSameNode adds a node-first preference. All are GA as of v1.35.

Why does my Service show endpoints in EndpointSlices but kubectl get endpoints shows fewer?

The legacy Endpoints object is capped at 1000 addresses and annotated endpoints.kubernetes.io/over-capacity: truncated. EndpointSlices have no such cap (100 per slice by default, across as many slices as needed). The Endpoints API is deprecated since v1.33.

How long does sessionAffinity ClientIP stick?

By default 10800 seconds (3 hours), configurable up to 86400 via sessionAffinityConfig.clientIP.timeoutSeconds. Stickiness is keyed on the source IP kube-proxy observes, which may be a node IP after SNAT.

Is this level of detail really on the CKA?

The Services & Networking domain (20%) requires using ClusterIP, NodePort and LoadBalancer Services and endpoints, and the Troubleshooting domain (30%) covers networking failures. You will not be asked to recite flag defaults, but you will be asked to make a Service work or preserve a client IP, and knowing the mechanism is what makes those tasks fast.

Conclusion & Next Steps

A Service is three objects and one controller working together: the Service that allocates the virtual IP, the EndpointSlices that list ready backends, and kube-proxy translating both into kernel rules. Every knob in this article narrows the set of backends a packet can reach — traffic policies by node, trafficDistribution by topology preference, sessionAffinity by client — and every one of them has a failure mode that looks like “the network is broken” until you read the Service spec.

Commit the four-step chain, the Cluster/Local table and the mode comparison to memory, then go break a cluster on purpose. When you can predict which node will drop traffic before you run curl, the Services & Networking domain stops being the part of the CKA you hope they do not ask about. Pair this article with the exam domains breakdown to see where networking tasks sit in your overall time budget, and use timed mock exams to confirm the debugging sequence survives the clock.

Limited Time Offer: Get 80% off all Mock Exam Bundles | Sale ends in 7 days. Start learning today.

Claim Now