IPVS mode now has a deletion date.
Kubernetes 1.37 shipped one deprecation and it is kube-proxy's ipvs mode. Off by default in 1.40, source deleted in 1.43. Here is the switch to nftables, the kernel floor, the four behaviours that change, and the trap that catches clusters not running ipvs at all.
- Kubernetes
- Networking
- Linux
- nftables
Kubernetes v1.37 landed on 26 August 2026 with 67 enhancements, of which exactly one is a deprecation — and it is the ipvs mode of kube-proxy. That is a smaller headline than gang scheduling or Pod certificates and a much larger operational item, because it is the only thing in the release that ends with code being deleted from a data plane that is running in production right now. The schedule is already written down: a feature gate in 1.37, off by default in 1.40, pkg/proxy/ipvs removed in 1.43.

There is a version of this that is a one-line ConfigMap edit and a version that is a bad Tuesday, and the difference is entirely in what you check first. What follows is the whole of it: the five KEP stages and which of them are dates rather than intentions, how to find out what your cluster is really running (including the answer that is worse than ipvs), why the mode was never the escape from iptables people remember it as, why the IPVS schedulers are not doing the job they are kept for, the kernel floor and which node images clear it, the four documented behaviour changes, a per-node rollout, the state that has to be cleaned up by hand, a verification script that exits non-zero, and a rollback that is genuinely two commands.
One deprecation shipped in 1.37, and this is it
None of the symptoms below says deprecation, which is why this tends to be noticed late — usually by whoever is doing the 1.40 upgrade, three releases after the warning first appeared. The mode does not degrade. It keeps working perfectly until the release where it does not exist, and the intervening warnings go into a log nobody greps.[rel137]
| What you are seeing | What it actually means | Where |
|---|---|---|
kube-proxy logs The ipvs proxier is now deprecated on every start | Stage 1 of KEP-5495, shipped in Kubernetes 1.35. Nothing is broken; the clock started two releases ago. | The five stages |
mode: is empty in the kube-proxy ConfigMap | The cluster has not chosen a data plane. Upstream will change the recommended default from iptables to nftables, and an unpinned cluster follows it. | Which mode |
| After upgrading to 1.40, kube-proxy exits with an error listing the valid modes | Stage 3. The KubeProxyIPVS gate now defaults to false and nothing set it back. | The five stages |
| A NodePort stopped answering on a secondary interface right after the switch | nftables mode defaults to --nodeport-addresses primary. This is the most common regression in this migration by a distance. | What changes |
| Traffic to one ClusterIP is dropped, on one node only | Stale addresses on a leftover kube-ipvs0 with no IPVS rules behind them. The node still answers ARP for an address it no longer serves. | Leftovers |
The second row of that table is the one worth reading twice, because it has nothing to do with IPVS and it catches clusters that never used it. The default mode in Kubernetes 1.37 is still iptables, and the documentation says plainly that a future release will change that default to nftables. If your kube-proxy configuration does not name a mode, you are not on iptables by decision — you are on whatever upstream currently recommends, and you have a data-plane replacement scheduled on somebody else's calendar.[vips]
Upstream's own recommendation, and the cheapest thing in this article: to avoid having the proxy backend in a cluster be changed unexpectedly during an upgrade, you should ensure that all clusters have a kube-proxy configuration that explicitly indicates which mode to use. That is a five-minute change and it is worth making today whichever mode you land on.
The five stages, and which two are dates
KEP-5495 sets this out in five stages, and the useful discipline is to separate the ones that are announcements from the ones that are dates. Stages 1 and 2 have already happened and changed nothing about how a cluster runs. Stage 3 changes a default, which means it changes what happens to a cluster where nobody did anything. Stage 4 removes the code, which is the only irreversible one.[kep5495]
| Stage | Kubernetes | What changes | What it asks of you |
|---|---|---|---|
| 1 | 1.35 | kube-proxy warns on startup in ipvs mode; the documentation is marked deprecated. nftables fixes backported to 1.33 and 1.34 so older clusters can still migrate. | Notice. Nothing else. |
| 2 | 1.37 | The KubeProxyIPVS feature gate is added: GA, default true. Behaviour is unchanged. | Plan. This is the last quiet release. |
| 3 | 1.40 | The gate flips to default false. kube-proxy started in ipvs mode without overriding it exits with an error listing the valid modes. | Be finished — or set the gate and buy three minor releases. |
| 4 | 1.43 | pkg/proxy/ipvs is removed from the tree. The feature gate can no longer bring it back; remaining mentions leave the docs. | Nothing. There is no ipvs mode. |
| 5 | 1.46 | The feature gate itself is removed. | Only relevant to tooling that inspects gates. |
Two of those rows deserve a note. The 1.46 cleanup stage exists in the KEP and in almost no coverage of this deprecation, which matters only if you are writing tooling that inspects feature gates — the gate is a real, listed, GA gate until then. And the reason SIG Network gave for doing this at all is worth knowing, because it is not performance: the group has no maintainers familiar with the ipvs backend code, and has been telling people who report ipvs bugs to move to nftables for some time. A backend nobody is fixing is a liability regardless of what its benchmarks used to say.[depol][k8srel]
Which mode is this cluster actually in
Start with what is actually configured, not what the runbook says, because on any cluster older than about two years those are different documents. There are three answers and the third one is the interesting one: ipvs, iptables, or nothing at all.[kpcfg]
# The one-liner from the v1.37 release announcement. On a kubeadm-built cluster
# the whole of kube-proxy's configuration lives in one ConfigMap key.
kubectl -n kube-system get configmap kube-proxy \
-o jsonpath='{.data.config\.conf}' | grep 'mode:'
# mode: ipvs
# Careful with the empty answer, because it is the most common one and it does
# NOT mean "iptables forever". An unset mode means "whatever kube-proxy decides
# is the recommended default", and upstream says in as many words that a future
# release will change that default from iptables to nftables. If this comes back
# blank, you have a data-plane change scheduled that nobody in your team chose.
# mode: "" <- pin it, whichever mode you intend to be on
# Managed clusters and non-kubeadm installers do not necessarily use that
# ConfigMap. Ask the process instead - this is true wherever the config came from.
kubectl -n kube-system get ds kube-proxy \
-o jsonpath='{.spec.template.spec.containers[0].args}' | tr ',' '\n'
# And from the node, which is the only answer that cannot be out of date:
ps -o args= -C kube-proxy
ipvsadm -L -n --daemon 2>/dev/null; ipvsadm -L -n | head -20
# IP Virtual Server version 1.2.1 (size=4096)
# Prot LocalAddress:Port Scheduler Flags
# TCP 10.96.0.1:443 rr
# -> 192.168.4.11:6443 Masq 1 3 0
# Not every cluster runs kube-proxy at all. If this returns nothing, your CNI
# is implementing Services itself and none of this deprecation reaches you.
kubectl -n kube-system get ds -l k8s-app=kube-proxyThen find the warning, which has been in your logs since Kubernetes 1.35 and is the cheapest proof that this applies to you. The feature gate added in 1.37 is worth locating in the same pass, not because you need to set it — it defaults to on and changes nothing this release — but because it is the lever you will be offered in 1.40, and it is better to know in advance that it only extends the deadline by three minor releases rather than removing it.[gates]
# Stage 1 of the deprecation landed in Kubernetes 1.35: kube-proxy logs a
# warning on every start in ipvs mode. It has been in your logs for two releases.
kubectl -n kube-system logs ds/kube-proxy --tail=-1 --prefix \
| grep -i -m5 'ipvs.*deprecat'
# The ipvs proxier is now deprecated and may be removed in a future release.
# Please use 'nftables' instead.
# Stage 2 is what 1.37 added: a feature gate. Today it is on by default, so
# nothing changes yet. In 1.40 the default flips and kube-proxy in ipvs mode
# exits with an error unless the gate is set back by hand; in 1.43 the gate
# cannot save you because the code is gone.
kubectl -n kube-system get configmap kube-proxy \
-o jsonpath='{.data.config\.conf}' | grep -A3 featureGates
# What the 1.40 override would look like. Write this down as the thing you are
# choosing NOT to depend on, rather than as a plan:
#
# featureGates:
# KubeProxyIPVS: true
#
# It buys three minor releases, or roughly nine months at the current cadence,
# and it stops working entirely in 1.43.
# Count how much of the fleet this is really about, because in most clusters
# the answer is "some node pools, historically".
kubectl get nodes -o custom-columns=\
'NODE:.metadata.name,KUBELET:.status.nodeInfo.kubeletVersion,'\
'KERNEL:.status.nodeInfo.kernelVersion,OS:.status.nodeInfo.osImage'What ipvs mode is, as opposed to what it is remembered as
It is worth being precise about what is being retired, because the popular version of the story is wrong in a way that changes the decision. The ipvs mode was added in Kubernetes 1.8 to get away from the cost of a rule list proportional to the number of Services. It did that. What it never did was replace iptables: the kernel IPVS API on its own cannot express the whole Kubernetes Service API — masquerade decisions, NodePort filtering, LoadBalancer source ranges, the reject behaviour for a Service with no endpoints — so the mode drives iptables and ipset underneath the IPVS table. The v1.37 announcement says exactly this. Count the rules on your own node if you would rather not take anyone's word for it.[kep3866]
# Three separate pieces of kernel state, which is the first surprise for anyone
# who believed ipvs mode meant "no iptables".
# (a) The IPVS virtual servers. This part is what people think of as ipvs mode.
ipvsadm -L -n | wc -l
# (b) A dummy interface holding every ClusterIP - and every LoadBalancer IP -
# as a /32 on the node. This is why the node answers ARP for addresses it
# does not own, and therefore why MetalLB in layer-2 mode needs strictARP.
ip -brief addr show kube-ipvs0 | head
# kube-ipvs0 DOWN 10.96.0.1/32 10.96.0.10/32 10.107.44.9/32 ...
# (c) The iptables rules and ipsets that ipvs mode drives underneath, because
# the kernel IPVS API on its own cannot express masquerade decisions,
# LoadBalancer source ranges, NodePort filtering or the reject rules for a
# Service with no endpoints. Count them before you claim to be iptables-free.
ipset list -name | grep -c '^KUBE-'
iptables-save -t nat | grep -c '^-A KUBE-'
# The scheduler in use, which is the setting this whole mode is usually kept for:
kubectl -n kube-system get configmap kube-proxy \
-o jsonpath='{.data.config\.conf}' | grep -A4 '^ipvs:'
# ipvs:
# scheduler: "lc"
# strictARP: true
# One field, for the whole cluster. There is no per-Service scheduler in
# Kubernetes: it is not in the Service API, and kube-proxy does not read one.
# Whatever is on that line is what every Service on every node gets.The second half of the popular version is the schedulers, and this is the reason most clusters that are still on ipvs are still on it. Someone set scheduler: "lc" years ago, and the cluster has been understood ever since to be doing least-connections load balancing. It is not, and the reason is architectural rather than a bug. kube-proxy runs on every node and each instance keeps its own IPVS table, which counts only the connections that this node opened. "Least connections" is therefore computed per client node: with twenty busy nodes you have twenty independent local decisions, not one global one, and traffic arriving from outside the cluster is not in any of those counts. SIG Network flagged this misunderstanding explicitly when it wrote the deprecation.[vips]
# The claim to test: "we run lc so connections go to the least loaded pod".
#
# Every node runs its own kube-proxy with its own IPVS table, and that table
# only counts connections this node opened. Ask two nodes about the same
# Service and the connection counts will not agree - because they are answers
# to different questions.
SVC_IP=$(kubectl get svc -n prod api -o jsonpath='{.spec.clusterIP}')
for node in $(kubectl get nodes -o name | head -3); do
echo "== ${node#node/}"
kubectl debug "$node" -q -it --image=busybox --profile=general -- \
chroot /host ipvsadm -L -n -t "$SVC_IP:8080" 2>/dev/null | tail -n +4
done
# == node-01
# -> 10.244.1.7:8080 Masq 1 118 4
# -> 10.244.2.4:8080 Masq 1 0 0 <- zero, from THIS node
# == node-02
# -> 10.244.1.7:8080 Masq 1 2 1
# -> 10.244.2.4:8080 Masq 1 96 3 <- the other pod, same Service
# Neither node is wrong. "Least connections" is being computed per client node,
# so with N busy client nodes you get N independent local decisions, not one
# global one. Add a client outside the cluster, or a client behind a
# LoadBalancer that lands on a different node, and IPVS never sees it at all.
#
# Clean up the debug pods. `kubectl debug node/...` names them
# node-debugger-<node>-<suffix> and sets no label, so match on the name.
kubectl get pods -o name | grep '^pod/node-debugger-' | xargs -r kubectl delete
# The two things people actually want from a scheduler have supported answers
# that survive this migration, and neither of them is an IPVS scheduler:
# sticky clients -> Service .spec.sessionAffinity: ClientIP
# keep it local -> Service .spec.internalTrafficPolicy: Local
kubectl get svc -A -o json | jq -r '
.items[] | select(.spec.sessionAffinity == "ClientIP")
| "\(.metadata.namespace)/\(.metadata.name) sessionAffinity=ClientIP"'| IPVS scheduler | What it does on one node | What that means for the cluster |
|---|---|---|
rr (the default) | Round robin over this node's endpoint list. | Functionally what iptables and nftables mode already do by selecting a backend at random. Nothing is lost here. |
lc, wlc, sed, nq | Fewest active connections, as counted by this node. | Not cluster-wide least-connections. N busy client nodes make N independent local decisions, and off-cluster traffic is in none of them. |
sh, dh | Hash on the source or destination address. | Frequently mistaken for session affinity. The supported equivalent is .spec.sessionAffinity: ClientIP, which is per Service and survives the migration. |
lblc, lblcr | Locality-based least connection. | Locality in Kubernetes is .spec.internalTrafficPolicy: Local, which the API actually knows about and the scheduler cannot see. |
mh (Maglev) | Consistent hashing. | kube-proxy always sets mh-port and never enables mh-fallback, so in practice it behaves as source hashing with ports. |
There is also no per-Service scheduler and never was. ipvs.scheduler is a single field in the kube-proxy configuration and it applies to every Service on every node — it is not part of the Service API, and no annotation changes it. Which is worth sitting with for a moment, because it means the two things people actually reach for the schedulers to get, sticky clients and node locality, have supported answers in the API that survive this migration untouched: sessionAffinity: ClientIP and internalTrafficPolicy: Local.[svc][stp]
| Mode | Status in 1.37 | Kernel | Uses iptables underneath | Where it ends |
|---|---|---|---|---|
iptables | The default. Not deprecated. | any | Yes, by definition | Stops being the default at some point; no removal is announced. |
ipvs | Deprecated since 1.35; feature gate added in 1.37 | any | Yes — masquerade, NodePort filtering, LoadBalancer source ranges, reject rules | Off by default in 1.40, source removed in 1.43. |
nftables | Stable since 1.33; the recommended replacement for ipvs | 5.13 or newer | No | Becomes the default in a future release. |
kernelspace | Windows nodes only | — | — | Unaffected by any of this. |
Can these nodes run nftables mode
Now the one hard constraint. The nftables mode requires Linux 5.13 or newer, and there is no partial support and no fallback — below it, kube-proxy will not run in that mode. This is the check that decides whether this migration is a ConfigMap edit or a node-image project, so do it before writing any plan.[vips]
#!/usr/bin/env bash
# Precheck. nftables mode needs Linux 5.13 or newer; there is no partial
# support and no fallback - kube-proxy will not start in nftables mode below it.
set -euo pipefail
need_major=5 need_minor=13
fail=0
while read -r node kernel os; do
ver=${kernel%%-*} # 6.8.0-51-generic -> 6.8.0
IFS=. read -r maj min _ <<<"$ver"
if (( maj > need_major )) || { (( maj == need_major )) && (( min >= need_minor )); }; then
printf ' ok %-22s %s\n' "$node" "$kernel"
else
printf ' TOO OLD %-20s %-24s %s\n' "$node" "$kernel" "$os"
fail=1
fi
done < <(kubectl get nodes -o custom-columns=\
'NAME:.metadata.name,KERNEL:.status.nodeInfo.kernelVersion,OS:.status.nodeInfo.osImage' \
--no-headers)
(( fail == 0 )) && echo "all nodes can run nftables mode" \
|| echo "some nodes cannot: rebuild the image or stay on iptables mode"
# On a node that reports too old, the answer is almost never "patch the kernel".
# It is "this node image is end of life". Check what the distro offers before
# planning anything: an Ubuntu 20.04 node on the HWE kernel is fine, the same
# release on the GA kernel is not.
uname -r
nft --version| Node image | Kernel it ships by default | nftables mode |
|---|---|---|
| RHEL 9, Rocky Linux 9, AlmaLinux 9 | 5.14 | Yes |
| RHEL 10 | 6.12 | Yes |
| RHEL 8, CentOS 7 | 4.18 and older | No — this is a node-image rebuild |
| Ubuntu 24.04 LTS | 6.8 | Yes |
| Ubuntu 22.04 LTS | 5.15 | Yes |
| Ubuntu 20.04 LTS | 5.4 GA, 5.15 with HWE | Only on the HWE kernel |
| Debian 12 | 6.1 | Yes |
| Debian 11 | 5.10 | No |
| Amazon Linux 2023 | 6.1 | Yes |
If some nodes do not clear the floor, the honest read is usually that the node image is end of life rather than that the kernel needs patching, and the correct fix is a rebuild. If a rebuild cannot happen before 1.40, the fallback is iptables mode rather than the feature gate: the iptables backend is not deprecated, its performance improved substantially after the ipvs mode was introduced, and upstream recommends it over ipvs for exactly this case. Worth adding for anyone weighing the timing: the deprecation notes that every kernel too old for nftables mode leaves long-term support by the end of 2026, which is the argument that the kernel objection has an expiry date of its own.[kernrel][kep5495]
Four things behave differently afterwards
Four behaviours change, and three of them are documented under a heading — migrating from iptables mode to nftables — that ipvs users have no reason to have read. They apply here identically, because they are properties of the destination rather than of the source.[vips]
| Behaviour | iptables / ipvs mode | nftables mode | What to do about it |
|---|---|---|---|
| NodePort listening addresses | All local addresses, unless narrowed | --nodeport-addresses primary by default | Audit which addresses are actually used, then set the option explicitly if you need more than the primary. |
NodePort on 127.0.0.1 | Works in iptables mode by default | Not available; restored in 1.37 behind an alpha gate | Read kubeproxy_iptables_localhost_nodeports_accepted_packets_total before deciding you do not need it. |
| Local firewall | kube-proxy adds accept rules per NodePort | Does nothing | Allow the NodePort range in your own host firewall. |
| Pre-6.1 conntrack reset bug | Workaround installed | Not installed by default | Check kubeproxy_iptables_ct_state_invalid_dropped_packets_total; if non-zero, carry --conntrack-tcp-be-liberal. |
| Your own rules on kube-proxy's chains | KUBE-SERVICES, KUBE-SEP-* exist in iptables | State lives in table ip kube-proxy / table ip6 kube-proxy | It was never API. Hook your own table in at the same netfilter priority instead. |
- NodePorts stop listening on every local address. In iptables and ipvs mode a
type: NodePortService is reachable on all local IPs unless you narrowed it. The nftables mode defaults to--nodeport-addresses primary, which means the node's primary IPv4 and/or IPv6 address from the Node object and nothing else. Anything that reaches a NodePort on a secondary NIC, a management address or a floating VIP stops working. Set the option explicitly if you need the old reach —0.0.0.0/0restores it — but audit first, because the default is the one upstream thinks people actually wanted. - Localhost NodePorts are a separate case, and 1.37 changed it. Connecting to
127.0.0.1:<nodePort>worked in iptables mode and did not work in nftables mode at all. As of Kubernetes 1.37 it can, behind the alphaKubeProxyNFTablesLocalhostNodePortsgate and withnodePortAddressesset toprimary,localhost. Before you decide whether you care, read the counter: kube-proxy has been counting packets accepted on loopback NodePorts all along. - kube-proxy stops opening your firewall for you. The iptables mode adds accept rules for each NodePort, on the theory that an over-aggressive local firewall would otherwise block them. That approach cannot work against an nftables-based firewall, so the nftables mode does nothing at all here. If you have a host firewall, it now has to allow the NodePort range itself — which is arguably how it should always have been, and is still a change.
- The conntrack workaround is not installed by default. Kernels before 6.1 have a bug that can reset long-lived TCP connections to Service IPs. The iptables mode installs a workaround; that workaround was later found to cause problems of its own, so the nftables mode leaves it out. Whether you need it is measurable rather than a judgement call — the counter is in the metrics — and if you do,
--conntrack-tcp-be-liberalis the supported way to get the behaviour back. - Anything of yours that matched kube-proxy's chains is now broken. If a firewall script, a CNI hook or a monitoring rule referred to
KUBE-SERVICESorKUBE-SEP-*by name, those chains are gone: kube-proxy's state now lives intable ip kube-proxyandtable ip6 kube-proxy. This was never supported — upstream has a standing blog post titled precisely that — but it is worth grepping for before the rollout rather than after, because it fails silently.
# The behaviour difference most likely to page you. In ipvs and iptables mode,
# NodePort Services are reachable on every local address unless you said
# otherwise. nftables mode defaults to --nodeport-addresses primary: the node's
# primary IPv4 and/or IPv6 address from the Node object, and nothing else.
#
# So the question to answer before the switch is: does anything reach a NodePort
# on a secondary address, a VIP, a management NIC, or on loopback?
# What is currently configured, if anything:
kubectl -n kube-system get configmap kube-proxy \
-o jsonpath='{.data.config\.conf}' | grep -i nodePortAddresses
# (empty means "all local addresses")
# Which NodePorts exist at all, and who might be pointed at them:
kubectl get svc -A -o json | jq -r '
.items[] | select(.spec.type == "NodePort" or .spec.type == "LoadBalancer")
| .spec.ports[]? | select(.nodePort)
| "\(.nodePort)\t\(.protocol)"' | sort -u
# Localhost NodePorts are the sharp edge: health checks, sidecars and a
# surprising number of monitoring agents connect to 127.0.0.1:<nodePort>.
# kube-proxy counts them for you, and a non-zero value means something out
# there depends on it.
kubectl -n kube-system exec ds/kube-proxy -- \
wget -qO- http://127.0.0.1:10249/metrics \
| grep kubeproxy_iptables_localhost_nodeports_accepted_packets_total
# kubeproxy_iptables_localhost_nodeports_accepted_packets_total 41822
# Kubernetes 1.37 gives that case a way out, as an alpha feature gate. If you
# need it, you need it on 1.37+ and you need both halves:
# featureGates:
# KubeProxyNFTablesLocalhostNodePorts: true
# nodePortAddresses: ["primary", "localhost"]
# And the conntrack workaround: iptables mode installs one for a pre-6.1 kernel
# bug that resets long-lived TCP connections. nftables mode does not, by default.
# Non-zero here means you are relying on it - carry --conntrack-tcp-be-liberal.
kubectl -n kube-system exec ds/kube-proxy -- \
wget -qO- http://127.0.0.1:10249/metrics \
| grep kubeproxy_iptables_ct_state_invalid_dropped_packets_totalThe pattern in all five is the same: none of them is a startup failure. kube-proxy comes up, reports healthy, serves most traffic correctly, and one specific path stops working. That is why the pre-flight below is worth more than the rollout itself, and why the first node stays on its own for a working day.[reset][ctsysctl]
The switch, one node at a time
The edit is three lines. The discipline is in deleting the ipvs block rather than leaving it next to the new mode, because dead configuration is how the next person concludes the cluster is still on ipvs. One thing to check on kubeadm-built clusters: this ConfigMap is regenerated by kubeadm upgrade, so a change made only with kubectl can be silently reverted at the next upgrade. Make it in the cluster configuration as well.[kubeadm]
# The change is three lines, and the discipline is in what you delete.
kubectl -n kube-system get configmap kube-proxy \
-o jsonpath='{.data.config\.conf}' > kube-proxy.conf.bak
cp kube-proxy.conf.bak kube-proxy.conf
# --- before ----------------------------------------------------------------
# mode: ipvs
# ipvs:
# scheduler: "lc"
# strictARP: true
# syncPeriod: 30s
#
# --- after -----------------------------------------------------------------
# mode: nftables
# nftables:
# minSyncPeriod: 1s
# syncPeriod: 30s
#
# Delete the whole ipvs block rather than leaving it. In nftables mode it is
# dead configuration: scheduler has no equivalent and no effect, and strictARP
# was only ever there to stop the node answering ARP for the ClusterIPs that
# ipvs mode bound onto kube-ipvs0 - an interface nftables mode never creates.
# Leaving it behind is how the next person concludes the cluster is still ipvs.
# Apply. Note that on a kubeadm cluster this ConfigMap is regenerated by
# `kubeadm upgrade`, so make the same change in the cluster configuration or
# the next upgrade will quietly put ipvs back.
kubectl -n kube-system create configmap kube-proxy \
--from-file=config.conf=kube-proxy.conf \
--dry-run=client -o yaml | kubectl apply -f -Then roll it out the way you would roll out anything that owns the data plane on every node, which is to say one node, then a pool, then the fleet. This one is unusually cheap to canary because the state is entirely per node and rebuilt from the API server on every start: there is nothing shared to corrupt, and a node that cannot run the new mode fails at startup with the kernel version in the message.[drain]
# Do not restart the DaemonSet across the fleet. kube-proxy owns the data plane
# on every node it runs on; a bad rollout is a cluster-wide outage, and this one
# is cheap to canary because the state is per node.
# Pause the DaemonSet so the ConfigMap change does not roll on its own.
kubectl -n kube-system patch ds kube-proxy \
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"maxUnavailable":1}}}}'
# One node. Cordon it, move the workloads off, restart only that pod.
NODE=node-07
kubectl cordon "$NODE"
kubectl drain "$NODE" --ignore-daemonsets --delete-emptydir-data --timeout=5m
kubectl -n kube-system delete pod \
--field-selector "spec.nodeName=$NODE" -l k8s-app=kube-proxy
# Watch it come up in the new mode. A node that cannot run nftables mode fails
# here, loudly, with the kernel version in the message - which is the correct
# place to find that out.
kubectl -n kube-system logs -f --tail=40 \
"$(kubectl -n kube-system get pod -l k8s-app=kube-proxy \
--field-selector "spec.nodeName=$NODE" -o name)"
kubectl uncordon "$NODE"
# Give it real traffic and a working day before the second node. The failures
# this migration produces are not startup failures; they are "one client on a
# secondary interface stopped reaching a NodePort", and that takes a shift to
# surface. Then a pool, then the fleet.What ipvs leaves behind
Cleanup is where this migration differs from every previous kube-proxy mode change, and in a good way. kube-proxy used to try to tidy up after the other modes and stopped — KEP-2448 removed that logic — because the iptables, ipvs and userspace backends all wrote into some of the same chains, so cleaning up one mode's rules deleted the running mode's rules too. nftables mode does not share anything: its whole state is in its own two tables. That is why switching in this direction is designed to remove the old rules on startup, and why cleaning up by hand is safe when it is needed.[kep2448][ipvsadm]
# kube-proxy in nftables mode is designed to remove the iptables and ipvs rules
# it finds on startup - the modes do not share state, which is exactly why this
# direction is safe when switching between the iptables-family modes was not.
# Verify rather than assume, on the first node, before the second one.
ipvsadm -L -n | tail -n +4 | wc -l # want 0
ip link show kube-ipvs0 2>/dev/null # want "does not exist"
ipset list -name | grep -c '^KUBE-' # want 0
iptables-save -t nat | grep -c '^-A KUBE-' # want 0 (or only your own rules)
# If something survived - an older kube-proxy, a node that was rebooted mid-way,
# a third party that wrote into those chains - clear it explicitly. Every one of
# these is safe once kube-proxy is confirmed running in nftables mode on the node.
ipvsadm --clear
ip link delete kube-ipvs0 # recreated only by ipvs mode
for s in $(ipset list -name | grep '^KUBE-'); do ipset destroy "$s"; done
# The stale kube-ipvs0 addresses are the ones that actually hurt. Left in place
# with no IPVS rules behind them, the node still claims those ClusterIPs and
# still answers ARP for them, and traffic that lands there is dropped rather
# than redirected. That is a black hole that looks like an application problem.
# The strictARP sysctls are set at runtime and do not revert on their own. They
# are harmless, but if you want the node back to stock:
sysctl -w net.ipv4.conf.all.arp_ignore=0
sysctl -w net.ipv4.conf.all.arp_announce=0
# There is also a supported flush. It is documented as cleaning up iptables and
# ipvs rules, so it is the right tool for this direction and the wrong one for
# the other. Run it with kube-proxy stopped on that node.
kube-proxy --cleanupOne leftover is worth more attention than the others. kube-ipvs0 is a dummy interface that ipvs mode uses to bind every ClusterIP — and every LoadBalancer IP — as a /32 on the node, which is the whole reason MetalLB in layer-2 mode documents strictARP as a requirement for ipvs clusters. nftables mode creates no such interface, so that requirement stops applying and the setting becomes dead config. But if the interface survives the switch with the addresses still on it and no IPVS rules behind them, the node keeps claiming those ClusterIPs and answering ARP for them while dropping the traffic. That is a black hole on one node that presents as an intermittent application failure, and it is the single best reason to verify the cleanup rather than assume it.[metallb][chains]
# Everything kube-proxy now owns lives in two tables of its own, one per IP
# family, which is the property that makes the whole thing inspectable.
nft list tables
# table ip kube-proxy
# table ip6 kube-proxy
# The Service map, which is the equivalent of what `ipvsadm -L -n` used to show.
# Note that it is a map lookup rather than a rule chain: this is the performance
# argument for the new backend, and it is visible in the output.
nft list table ip kube-proxy | head -40
# One Service end to end:
SVC_IP=$(kubectl get svc -n prod api -o jsonpath='{.spec.clusterIP}')
nft list table ip kube-proxy | grep -A3 "$SVC_IP"
# Live rule changes, which is the closest thing to watching kube-proxy think:
nft monitor rules
# What kube-proxy does NOT own any more - and must not, if the cleanup worked:
nft list ruleset | grep -c 'KUBE-SVC\|KUBE-SEP' # want 0
ipvsadm -L -n | tail -n +4 | wc -l # want 0
# A standing caution that predates all of this: kube-proxy's chains and tables
# are not API. If something of yours matched on KUBE-SERVICES by name, it is
# broken now, and it was unsupported before. Hook your own table into the same
# netfilter priorities instead of writing into kube-proxy's.Verifying, rather than hoping
Verification has a specific shape on this migration, dictated by the failure modes: almost everything that goes wrong leaves kube-proxy running and healthy. So checking that the pod is Running establishes nothing at all. The script below checks the mode kube-proxy actually chose rather than the one in the ConfigMap, that no ipvs state survived, that the dummy interface is gone, that rule sync is succeeding — and then does the one thing no inspection can do, which is open a real connection to a real Service from a pod on that node.[dbgsvc]
#!/usr/bin/env bash
# Run on a migrated node, from a machine with kubectl and cluster access.
# Exits non-zero on anything that would be silently wrong. The failures this
# migration produces do not stop kube-proxy, so "the pod is Running" proves
# nothing at all.
set -uo pipefail
NODE=${1:?usage: verify.sh <node>}
NS=${NS:-default}
rc=0
say() { printf '%-46s %s\n' "$1" "$2"; }
chk() { if [[ $2 == "$3" ]]; then say "$1" "ok"; else say "$1" "FAIL ($2 != $3)"; rc=1; fi; }
POD=$(kubectl -n kube-system get pod -l k8s-app=kube-proxy \
--field-selector "spec.nodeName=$NODE" -o jsonpath='{.items[0].metadata.name}')
# 1. the mode kube-proxy actually chose, not the one in the ConfigMap
mode=$(kubectl -n kube-system logs "$POD" | grep -om1 'Using .* Proxier' | awk '{print $2}')
chk "proxy mode" "$mode" "nftables"
# 2. no ipvs state left on the node
left=$(kubectl debug "node/$NODE" -q --image=busybox --profile=general -- \
chroot /host sh -c 'ipvsadm -L -n 2>/dev/null | tail -n +4 | wc -l' 2>/dev/null | tr -d ' ')
chk "ipvs virtual servers remaining" "${left:-0}" "0"
# 3. the dummy interface is gone, so no stale ClusterIP black holes
iface=$(kubectl debug "node/$NODE" -q --image=busybox --profile=general -- \
chroot /host sh -c 'ip link show kube-ipvs0 >/dev/null 2>&1 && echo present || echo absent' \
2>/dev/null | tr -d ' ')
chk "kube-ipvs0" "${iface:-absent}" "absent"
# 4. sync is succeeding, which is the counter that replaces "is it up"
m=$(kubectl -n kube-system exec "$POD" -- wget -qO- http://127.0.0.1:10249/metrics)
fails=$(awk '/^kubeproxy_sync_proxy_rules_nftables_sync_failures_total/ {s+=$2} END{print s+0}' <<<"$m")
chk "nftables sync failures" "$fails" "0"
awk '/^kubeproxy_sync_proxy_rules_last_timestamp_seconds/ {print " last successful sync:", $2}' <<<"$m"
# 5. and the part no inspection can establish: a real connection to a real
# Service, from a pod on this node, plus a NodePort from off-box.
kubectl -n "$NS" run "nftcheck-$$" --rm -i --restart=Never \
--overrides="{\"spec\":{\"nodeName\":\"$NODE\"}}" \
--image=curlimages/curl -- \
curl -sS -o /dev/null -w '%{http_code}\n' --max-time 5 \
http://kubernetes.default.svc.cluster.local:443 >/dev/null 2>&1 \
&& say "in-cluster Service connect" "ok" \
|| { say "in-cluster Service connect" "FAIL"; rc=1; }
exit $rcAfterwards, three counters are worth an alert for a fortnight. Note what is missing from the list: the Kubernetes metrics reference has iptables-specific counters and nftables-specific counters and nothing whatsoever for ipvs. Whatever monitoring you have for ipvs mode today, you built it yourself out of ipvsadm — which is a small, concrete illustration of the maintenance argument behind the whole deprecation.[metrics]
| Metric | What it tells you | Worth alerting on |
|---|---|---|
kubeproxy_sync_proxy_rules_nftables_sync_failures_total | kube-proxy cannot write the ruleset, so the data plane is drifting from the API server. | Any increase, permanently. |
kubeproxy_sync_proxy_rules_nftables_cleanup_failures_total | It cannot remove rules it believes are stale — usually leftovers from the old mode, or something else writing into the same tables. | Any increase for the fortnight after the switch. |
kubeproxy_sync_proxy_rules_last_timestamp_seconds | When the node's rules last matched the API server. This is the one that catches the silent failure. | More than five minutes in the past. |
kubeproxy_iptables_localhost_nodeports_accepted_packets_total | Something is connecting to a NodePort over loopback. | Non-zero before you switch, as a blocker. |
kubeproxy_sync_proxy_rules_duration_seconds | How long a full sync takes. The performance argument for the migration, if you have enough Services for one. | Baseline it before and after; no alert. |
# kube-proxy serves these on 10249 on every node. Three of them are worth an
# alert for the fortnight after the migration; the rest are for the postmortem.
# Sync is failing on this node - the data plane is drifting from the API server:
sum by (node) (rate(kubeproxy_sync_proxy_rules_nftables_sync_failures_total[5m])) > 0
# Cleanup is failing - usually leftovers from the previous mode, or something
# else writing into the same tables:
sum by (node) (rate(kubeproxy_sync_proxy_rules_nftables_cleanup_failures_total[5m])) > 0
# Rules are stale. This is the one that catches the silent failure, because
# kube-proxy stays Running while it happens:
time() - max by (node) (kubeproxy_sync_proxy_rules_last_timestamp_seconds) > 300
# And the pre-migration baseline worth keeping: sync duration before and after.
# In clusters with a few thousand Services the improvement is the point; in a
# cluster with forty Services there is nothing to see and that is fine too.
histogram_quantile(0.99, sum by (le) (rate(kubeproxy_sync_proxy_rules_duration_seconds_bucket[5m])))
# Note what is not in this list. The Kubernetes metrics reference has
# iptables-specific counters and nftables-specific counters and no ipvs-specific
# counter at all. Whatever observability you have for ipvs mode today, you built
# it yourself out of ipvsadm - which is its own argument about maintenance.Rolling back, and what it buys
Rollback is genuinely cheap here, which is not true of the other migrations landing on these nodes this year. There is no data to convert, no on-disk format to downgrade, and no state that outlives a restart: the entire change is one ConfigMap key plus per-node kernel state that kube-proxy rebuilds from the API server every time it starts. Put the old key back, delete the pod on that node, and the node is where it was in under a minute.[kep3866]
# Rollback here is genuinely cheap, which is not true of most migrations on
# these nodes. There is no data to convert and no format to downgrade: the whole
# of the change is one ConfigMap key and per-node kernel state that is rebuilt
# from the API server on every start.
kubectl -n kube-system create configmap kube-proxy \
--from-file=config.conf=kube-proxy.conf.bak \
--dry-run=client -o yaml | kubectl apply -f -
kubectl -n kube-system delete pod \
--field-selector "spec.nodeName=$NODE" -l k8s-app=kube-proxy
# kube-proxy in ipvs mode removes the nftables rules it finds on startup. If it
# does not - and rollback matters most exactly when the new backend is
# misbehaving - the manual version is two commands, because every rule
# kube-proxy owns is inside its own tables:
nft delete table ip kube-proxy
nft delete table ip6 kube-proxy
# Then confirm the old data plane is actually back, rather than assuming:
ipvsadm -L -n | tail -n +4 | wc -l # want non-zero again
ip -brief addr show kube-ipvs0 # the dummy interface returns
# Be honest about what the rollback bought. ipvs mode is off by default in
# Kubernetes 1.40 and the code is deleted in 1.43. A rollback ends a bad
# maintenance window; it does not move the date.Two caveats, both about scope rather than mechanism. kube-proxy in ipvs mode is designed to delete the nftables rules it finds on startup, but rollback matters most precisely when the new backend is misbehaving, so it is worth knowing the manual version: every rule kube-proxy owns is inside its own tables, and two nft delete table commands remove all of it. And be clear about what the rollback actually buys. It ends a bad maintenance window. It does not move 1.40, and it does not move 1.43.[skew]
The other answer: no kube-proxy at all
There is a second answer to this deprecation that is worth naming, because for some clusters it is the better one: stop running kube-proxy. Cilium and Calico both implement Kubernetes Services directly in eBPF, with a hash-map lookup instead of a rule list, and both document running with kube-proxy removed rather than alongside it. If you are already running one of them, the ipvs deprecation may be an invitation to make a change you were going to make anyway.[cilium]
It is a much larger change than the one this article describes, and it should be weighed as one. Replacing kube-proxy moves Service implementation into the CNI, which couples your data plane to that project's release cadence, its kernel requirements — which are higher than 5.13, not lower — and its debugging tools, and it is not something to do inside a deprecation window under time pressure. The honest sequencing for most clusters is: move to nftables now because it is a ConfigMap edit, and evaluate the eBPF question on its own merits and its own schedule.[calico][netpol]
The order to do this in
Compressed, the decision is much smaller than the article. If your kernels are 5.13 or newer, this is a configuration change with a careful rollout. If they are not, it is a node-image project, and the interim answer is iptables mode rather than the feature gate. And if your kube-proxy configuration does not name a mode at all, that is the thing to fix this week regardless of everything else here.[kep5495]
| Where you are | What to do |
|---|---|
| ipvs mode, every node on 5.13 or newer | Switch to nftables. That is the whole of this article, and it is a ConfigMap edit plus a careful rollout. |
| ipvs mode, some nodes below 5.13 | Rebuild those node images. If that cannot happen before 1.40, move them to iptables mode rather than setting the feature gate — iptables is not deprecated and upstream recommends it over ipvs for old kernels. |
| iptables mode, no plan | Pin mode: iptables explicitly today so an upgrade cannot change it, then move to nftables when your kernel floor allows it. |
| No mode set at all | Pin it this week, whichever one you want. This is the item with a deadline nobody in your organisation chose. |
| The CNI already replaces kube-proxy | Nothing to do. Confirm kube-proxy really is not running rather than assuming, because partial replacements exist. |
- Pin the mode, whatever it is. If
mode:is empty in the kube-proxy ConfigMap, set it explicitly today. This is the only item on the list that applies to clusters with no ipvs anywhere, and it is the one with a deadline you did not choose. - Check the kernels before you write a plan. One command across the fleet decides whether this is an afternoon or a quarter. Nodes below 5.13 need a new image, not a patch.
- Measure the two counters that decide the sharp edges. Localhost NodePorts and the conntrack invalid-state drops are both answerable from kube-proxy's own metrics, before you change anything. Audit the NodePort listening addresses in the same pass.
- One node, a full working day, then a pool. The regressions here are not startup failures; they are a single client on a secondary interface, and that takes a shift to surface. Verify the cleanup of
kube-ipvs0and the ipsets on that first node by hand. - Delete the ipvs block and the strictARP setting. Neither does anything in nftables mode, and leaving them is how the next person concludes the migration never happened. Then make the same change wherever the ConfigMap is generated from, or the next
kubeadm upgradewill undo it.
This is one of several changes landing on the same nodes in the same year, and they read better together: the move from Ingress NGINX to the Gateway API, which is the other half of the networking work and shares the same rollout discipline; migrating containerd 1.7 to 2.x, the runtime underneath on its own retirement schedule; and the cgroup v1 to v2 migration, which is the node-level change Kubernetes has already made mandatory. If you are weighing how much of this you need at all, when not to use Kubernetes is the other side of the argument.
Frequently asked questions
Is the ipvs mode of kube-proxy removed in Kubernetes 1.37?
No. Kubernetes 1.37 adds the KubeProxyIPVS feature gate, which defaults to true, so ipvs mode behaves exactly as it did before. The dates that matter are 1.40, when the gate flips to false by default and kube-proxy in ipvs mode exits with an error unless you override it, and 1.43, when pkg/proxy/ipvs is deleted and the gate can no longer help. The gate itself is removed in 1.46.
How do I find out which proxy mode my cluster is using?
On a kubeadm-built cluster, kubectl -n kube-system get configmap kube-proxy -o jsonpath='{.data.config\.conf}' | grep 'mode:'. If that comes back empty, the cluster has not chosen a mode and is running whatever kube-proxy currently recommends — which is iptables in 1.37 and will become nftables in a future release. On a cluster that does not use that ConfigMap, read the DaemonSet arguments instead, or run ipvsadm -L -n on a node.
Do I have to migrate to nftables, or can I use iptables mode?
iptables mode is a legitimate destination. It is not deprecated, it has no kernel floor, and its performance improved substantially in the years after ipvs mode was introduced — upstream explicitly recommends it over ipvs for systems too old to run nftables mode. nftables is the better target where the kernel allows it, and it is where the default is heading, but "ipvs to iptables" is a real answer for old node images rather than a cop-out.
What kernel version does kube-proxy's nftables mode need?
Linux 5.13 or newer, on Linux nodes only. There is no partial support: kube-proxy will not run in nftables mode below that. In practice that rules out RHEL 8, CentOS 7, Debian 11 and Ubuntu 20.04 on the GA kernel, and clears RHEL 9 and 10, Debian 12, Ubuntu 22.04 and 24.04, and Amazon Linux 2023. Check with kubectl get nodes -o custom-columns=NAME:.metadata.name,KERNEL:.status.nodeInfo.kernelVersion rather than by distribution name.
Will I lose the IPVS scheduler I configured?
You will lose the setting, and almost certainly not the behaviour you thought it was giving you. ipvs.scheduler is one field for the entire cluster, not per Service, and each node's IPVS table only counts connections that node opened — so lc is least-connections per client node rather than across the cluster. If what you wanted was sticky clients, that is .spec.sessionAffinity: ClientIP on the Service. If it was node locality, that is .spec.internalTrafficPolicy: Local. Both are unaffected by the migration.
Do I still need strictARP after moving to nftables mode?
No. strictARP exists because ipvs mode binds every ClusterIP and LoadBalancer IP onto the kube-ipvs0 dummy interface, which makes the node answer ARP for addresses MetalLB is trying to control. nftables mode never creates that interface, so the underlying problem is gone and the setting is dead configuration — delete it with the rest of the ipvs block. Do verify that kube-ipvs0 is actually gone on each migrated node, because a leftover interface with stale addresses will black-hole traffic.
Is it safe to switch modes on a running cluster?
Switching between an iptables-family mode and nftables is designed to be: nftables mode keeps all of its state in its own two tables and removes the iptables and ipvs rules it finds at startup, and the reverse holds on rollback. That is specifically different from switching between iptables and ipvs, which shared chains and is why kube-proxy's automatic cleanup was removed in KEP-2448. It is still a data-plane change on every node it touches, so cordon, drain, restart one kube-proxy pod, verify, and leave that node alone for a working day.
What breaks most often in this migration?
NodePort reachability. nftables mode defaults to --nodeport-addresses primary, so a NodePort that was being reached on a secondary NIC, a management address, a floating VIP or on 127.0.0.1 stops answering — while kube-proxy stays healthy and everything else keeps working. Both cases are measurable in advance from kube-proxy's own metrics and from the current nodePortAddresses setting.
Should I replace kube-proxy with Cilium or Calico eBPF instead?
It is a real option and for some clusters the better one, but it is a much larger change than a mode switch: it moves Service implementation into the CNI and couples your data plane to that project's releases, kernel requirements and debugging tools. Deciding it under deprecation pressure is the wrong way round. Moving to nftables is a ConfigMap edit that removes the deadline; evaluate the eBPF question afterwards, on its own schedule.
How do I roll back if nftables mode misbehaves?
Restore the previous ConfigMap key and delete the kube-proxy pod on that node; the old mode rebuilds its state from the API server on startup, so the node is back in under a minute. If kube-proxy's own cleanup does not run — which is exactly the case where rollback matters — every rule it owns is inside its own tables, so nft delete table ip kube-proxy and nft delete table ip6 kube-proxy remove all of it. The rollback ends a bad maintenance window; it does not move 1.40 or 1.43.
One release-level note, because the ledger for 1.37 is not what most coverage says it is: the change that can actually leave pods in ContainerCreating is SELinuxMount reaching GA, while the cgroup v1 failure landed in 1.35, the static pod restriction in 1.34, and the containerd cliff is still ahead in 1.38. what actually breaks when you upgrade to Kubernetes 1.37 separates the three columns and gives the audit to run before the upgrade rather than after it.
Sources
Primary sources only, checked on 27 August 2026. Where the Kubernetes documentation and a secondary write-up disagree about a version number in this deprecation, the documentation and the KEP are the ones to trust — the staging has been revised once already.
- Kubernetes - Virtual IPs and Service Proxies: the reference page for every kube-proxy mode. It carries the deprecation notice for ipvs mode with the 1.40 and 1.43 dates, the kernel 5.13 requirement for nftables mode, the full list of IPVS schedulers and the ipvs.scheduler field they are set through, the four documented behaviour differences when migrating to nftables, and the sentence that matters most to clusters that are not on ipvs at all: the default mode is iptables in 1.37 and a future release will change it to nftables
- Kubernetes v1.37: Garhwal - the release announcement of 26 August 2026. One of the 67 enhancements is a deprecation and it is this one. The deprecation section states the timeline, gives the jsonpath one-liner for finding out which mode a cluster is running, and says outright that ipvs mode continues to use iptables underneath because the kernel IPVS API alone cannot implement Kubernetes Services
- KEP-5495: Deprecate ipvs mode in kube-proxy - the enhancement proposal itself, and the only document that carries all five stages. Stage 2 in 1.37 adds the KubeProxyIPVS feature gate; stage 3 in 1.40 flips it off by default; stage 4 in 1.43 removes pkg/proxy/ipvs; the cleanup stage in 1.46 removes the gate. It also records why: SIG Network has no maintainers familiar with the ipvs backend, and the kernels too old for nftables mode will be out of LTS by the end of 2026
- KEP-3866: Add an nftables-based kube-proxy backend - the design document for the mode you are migrating to, including the section titled "The ipvs mode of kube-proxy will not save us", the reasoning behind switching modes being safe in this direction when it was not between the iptables-family modes, and the two nft commands that remove every rule kube-proxy owns
- KEP-265: IPVS load balancing mode in Kubernetes - the original 2017 proposal, worth reading now mainly to see which of its promises were kept and which were quietly not
- KEP-2448: Remove kube-proxy automatic clean-up logic - why kube-proxy stopped trying to tidy up after the other modes, and therefore why the cleanup step in this migration is something you do rather than something that happens
- kube-proxy command line reference: --proxy-mode, --cleanup, --nodeport-addresses, --conntrack-tcp-be-liberal, --ipvs-scheduler, --ipvs-strict-arp and the rest. Note that --cleanup is documented as cleaning up iptables and ipvs rules, which is the direction that matters here
- KubeProxyConfiguration API reference: the schema of the config.conf that lives in the kube-proxy ConfigMap, including the mode field, the ipvs and nftables sections, and which options are read in which mode
- Kubernetes feature gates: where KubeProxyIPVS and KubeProxyNFTablesLocalhostNodePorts are listed with their stage and default, and the reference for how a gate is passed to a component that is not the API server
- Kubernetes metrics reference: the exact names of the kube-proxy counters used in this article, including kubeproxy_sync_proxy_rules_nftables_sync_failures_total, kubeproxy_iptables_ct_state_invalid_dropped_packets_total and kubeproxy_iptables_localhost_nodeports_accepted_packets_total. Also, by omission, the fact that there is no ipvs-specific counter anywhere in the list
- Kubernetes deprecation policy: what a deprecation of a component flag or behaviour actually commits the project to, which is the frame for reading the KEP-5495 stages as dates rather than intentions
- Kubernetes releases: the supported branches and their end-of-life dates, which is how you turn "1.40" and "1.43" into calendar quarters for your own cluster
- Kubernetes version skew policy: how far kube-proxy is allowed to lag the API server and the kubelet, which bounds how long a partially migrated fleet can stay partially migrated
- Kubernetes v1.35: Timbernetes - the release where stage 1 of this deprecation landed and kube-proxy started logging a warning on startup in ipvs mode. If nobody in your organisation noticed, that is the point
- Kubernetes v1.36: Haru - the release in between, useful for placing the deprecation on the same timeline as the other node-level changes of 2026
- Kubernetes v1.37 sneak peek: the pre-announcement of the same deprecation, published four weeks before the release
- Kubernetes - Service: the API that all of this implements, and the reference for sessionAffinity, which is the feature people mistakenly believe the IPVS sh scheduler is providing
- Kubernetes - EndpointSlices: the objects kube-proxy actually watches, and the reason rule-sync cost scales with endpoint churn rather than with Service count alone
- Kubernetes - Service internal traffic policy: the supported way to keep traffic on the local node, which is the thing IPVS locality-based schedulers are sometimes reached for instead
- Kubernetes - Cluster networking: where kube-proxy sits relative to the CNI plugin, which decides whether any of this applies to your cluster at all
- Kubernetes - Debug Services: the official checklist for a Service that does not answer, and the first thing to run when a node comes back on a new proxy mode
- Kubernetes - Safely drain a node: the cordon, drain and uncordon sequence this migration slots into, one node at a time
- Kubernetes - Upgrading kubeadm clusters: for kubeadm-built clusters, the place the kube-proxy DaemonSet and its ConfigMap come from, and the reason a config change can be reverted by the next upgrade if it is not also made in the cluster configuration
- Kubernetes blog - Kubernetes's iptables chains are not API: the standing warning that anything of yours which hooks into kube-proxy's own chains is unsupported. It is the single best predictor of what will break when the chains are replaced by nftables tables
- Kubernetes blog - IPVS-based in-cluster load balancing deep dive: the 2018 introduction to the mode being retired, including the kube-ipvs0 dummy interface and the ipset usage that this article tells you to go and clean up
- Kubernetes blog - kube-proxy subtleties, debugging an intermittent connection reset: the original write-up of the conntrack invalid-state problem whose workaround nftables mode does not install by default
- kubernetes/kubernetes, pkg/proxy/ipvs: the directory KEP-5495 stage 4 deletes. Worth a look if you want to see for yourself how much iptables the ipvs mode is driving
- kubernetes/kubernetes, pkg/proxy/nftables: the implementation you are moving to, and the authority on which table and chain names to expect on a migrated node
- nftables wiki: the syntax reference for reading what kube-proxy now writes, in particular sets, maps and verdict maps, which are the features the iptables API cannot express and the reason the new backend is faster
- nft(8) manual page: list, delete, monitor and the ruleset commands used in the verification section
- The netfilter project's nftables page: the upstream statement of what nftables replaces and why development moved there
- Linux kernel documentation - nf_conntrack sysctls: nf_conntrack_tcp_be_liberal, which is what --conntrack-tcp-be-liberal sets, and the surrounding timeouts kube-proxy also manages
- kernel.org - active kernel releases: the longterm branches and their projected end-of-life dates, which is how to check the KEP's claim that every kernel too old for nftables mode leaves LTS by the end of 2026
- ipvsadm(8): the tool for reading and clearing the IPVS table that kube-proxy leaves behind, including -L -n for inspection and -C for the flush used in the cleanup step
- MetalLB installation: the source of the strict ARP requirement for kube-proxy in ipvs mode. It is an ipvs-only requirement because it works around an ipvs-only behaviour, which is why it stops applying after this migration
- Cilium - Kubernetes without kube-proxy: the other answer to this deprecation, which is to stop running kube-proxy at all and let an eBPF data plane implement Services
- Calico - enabling the eBPF data plane: the same answer from the other major CNI, including the requirement to disable kube-proxy rather than run both
Was this useful?