Core engineering · Practical technical guide · 1 November 2026
What externalTrafficPolicy changes for LoadBalancer and NodePort Services: why Cluster hides the client IP behind an extra hop, how Local preserves it but can skew load and drop traffic, how the health check node port and terminating endpoints keep rollouts safe, and how to verify it.
The question behind the field
The storefront of Belgavi's Book Store runs behind a cloud load balancer created by a Kubernetes Service of type LoadBalancer. Two requests come in during the same week: the fraud team wants the real client IP address in the access logs, and the platform team wants to know why one node's pods run hotter than the rest. Both answers are the same field, spec.externalTrafficPolicy, which has two values, Cluster (the default) and Local.
What each value does
| Cluster | Local | |
|---|---|---|
| Which endpoints a node forwards to | Any ready endpoint in the cluster | Only ready endpoints on the same node |
| Client source IP at the pod | Usually replaced by a node address (SNAT) | Preserved |
| Extra network hop | Often: node to node | None |
| Nodes without an endpoint | Forward traffic to other nodes | Do not forward traffic for the Service |
| Load balancer health check | kube-proxy readiness, port 10256 /healthz | healthCheckNodePort, 200 only if the node has a local endpoint |
The Kubernetes documentation describes it this way: Cluster routes external traffic to all ready endpoints, and Local only to ready endpoints on the node that received it; if there are none, kube-proxy does not forward the traffic. With Cluster, the receiving node may send the packet to a pod on another node, and to make the reply come back through it, it rewrites the source address, so the pod sees a node IP. With Local, the packet never leaves the node it arrived on, so no rewrite is needed and the pod sees the client.
A manifest with Local
Illustrative Service manifest; names and ports are examples.
apiVersion: v1
kind: Service
metadata:
name: storefront
spec:
type: LoadBalancer
externalTrafficPolicy: Local # keep the client IP, no cross-node hop
healthCheckNodePort: 32122 # optional; allocated from the NodePort range if omitted
selector:
app: storefront
ports:
- port: 443
targetPort: 8443Changing the policy on an existing Service is an in-place update, but the cloud integration has to reconfigure the load balancer health check to the new port, so do it in a quiet period and watch the target health in the provider console. Pair it with topology spread constraints on the Deployment so that each node holds a similar number of pods.
How Local stays correct: the health check node port
Local only works because the load balancer stops sending traffic to nodes without endpoints. For a LoadBalancer Service with Local, Kubernetes allocates spec.healthCheckNodePort from the NodePort range unless you set one, and kube-proxy serves /healthz on it on every node. According to the documentation it returns 200 only when kube-proxy is healthy and the node has a local endpoint for the Service. Cloud integrations point the load balancer's health check at that port. You can watch it directly:
Illustrative commands; the port and node addresses are placeholders.
kubectl get svc storefront -o jsonpath='{.spec.healthCheckNodePort}'
# 32122
curl -s http://NODE_A_IP:32122/healthz # node with a pod: 1 Service Endpoints found
curl -s http://NODE_B_IP:32122/healthz # node without: No Service Endpoints FoundThere is a window, though. The load balancer probes on an interval and needs several failures before it removes a node, so for a few seconds after the last local pod leaves, the load balancer can still send connections to a node that will not forward them.
Rollouts and terminating endpoints
That window is why the documentation describes traffic to terminating endpoints, stable since Kubernetes 1.28. With Local, if all of a node's local endpoints are terminating, kube-proxy forwards to those terminating endpoints instead of dropping traffic, and otherwise always prefers endpoints that are not terminating. During a rolling update a node can go from N replicas to 0; until the load balancer's health check marks the node down, new connections still reach the terminating pods, which keep serving while they drain. This only helps if the pods actually keep serving after receiving SIGTERM: a preStop delay and a graceful shutdown that finishes in-flight work, as in any zero-drop termination setup.
Uneven load: the arithmetic
With Local, most cloud load balancers spread connections across healthy nodes, not across pods. If the storefront has 6 pods on 3 nodes, placed 4, 1 and 1, each node gets a third of the traffic, so the pod alone on a node gets a third and each of the four pods on the busy node gets a twelfth: a four-to-one skew between pods.
| Placement | Traffic share per pod | Busiest versus quietest pod |
|---|---|---|
| 2, 2, 2 on three nodes | 1/6 each | 1 : 1 |
| 4, 1, 1 | 1/12, 1/3, 1/3 | 4 : 1 |
| 5, 1 on two nodes | 1/10 and 1/2 | 5 : 1 |
These shares are arithmetic under the assumption that the load balancer weights nodes equally; some load balancers can weight by endpoint count or send traffic straight to pod IPs, which removes the skew. Topology spread constraints or pod anti-affinity keep pods evenly placed, and that matters more with Local than with Cluster.
Choosing
- Cluster when you do not need the client IP at the pod, pods are few or unevenly placed, and an extra hop is acceptable. It is the forgiving default.
- Local when the client IP matters for logging, rate limiting or allow-lists, when you want to avoid the cross-node hop, and when you can keep pods spread across nodes.
- Neither, if your load balancer can target pod IPs directly; the node-level policy then matters less, and the client IP is visible if the load balancer passes it through.
- For HTTP, an
X-Forwarded-Forheader set by an L7 proxy you control is often a simpler source of the client IP thanLocal, as long as you trust only the hop you control.
Verification checklist
- Deploy an echo server and confirm the source address it logs under each policy, from a client outside the cluster.
- Curl the health check node port on every node and check that 200 appears exactly on nodes with ready pods.
- Run a rolling update under steady load with Local and count failed connections; repeat with preStop removed to see the difference.
- Compare requests per pod from your metrics with the placement; a skew that matches the table is the policy, not the application.
- Check that internalTrafficPolicy, a separate field, is what you intend for traffic from inside the cluster.
Failure modes
- Scaling to one replica. With Local, a Deployment scaled to one pod serves through one node; a node drain makes the Service unavailable until the new pod is ready.
- Health check misconfigured. A load balancer probing the traffic port instead of the health check node port sends traffic to nodes that drop it.
- Custom proxies. Replacements for kube-proxy need an equivalent health endpoint, as the documentation notes.
- Assuming the client IP is trustworthy. Preserved does not mean authenticated; a NAT or proxy in front still hides the real client.
Sources and scope
Checked against these sources on 1 November 2026. Behaviour described is from the Kubernetes documentation read on 1 November 2026. Traffic shares are arithmetic under an equal-weight load balancer, not measurements; no cluster was run for this guide.
externalTrafficPolicy Local keeps the client IP and avoids a hop, but traffic follows nodes, not pods, and relies on the health check node port. Spread pods evenly, keep preStop drains so terminating endpoints can serve, and verify the source IP, health checks and per-pod load.