iptables vs eBPF: why Cilium changes the networking model at scale
One traverses a growing rule chain. One doesn't.
iptables and eBPF solve the same problem — deciding what happens to a packet — using fundamentally different mechanisms. The difference doesn’t matter at 50 services. It matters a great deal at 5,000.
How iptables actually processes a packet
kube-proxy manages iptables rules on every node. Every Kubernetes Service generates a set of rules for load balancing and NAT. When a packet arrives, it walks through chains in order — PREROUTING, then FORWARD, then POSTROUTING — checking each rule sequentially until one matches.
packet arrives at NIC
│
▼
kernel network stack
│
▼
iptables PREROUTING chain
│
▼
traverse rules linearly — O(n)
│
▼
FORWARD chain
│
▼
POSTROUTING / NAT
│
▼
delivered to pod
The word that matters here is linearly. At 50 services you might have a few hundred rules. At 5,000 services, Google’s own published benchmarks show iptables rule processing becoming a measurable latency source — every single packet checks every relevant rule in sequence, and that list only grows as the cluster grows.
How eBPF changes the mechanism, not just the implementation
eBPF lets you run sandboxed programs directly inside the Linux kernel, attached to network hooks — XDP at the NIC level, before the packet even enters the full network stack. Cilium uses this to replace iptables rule chains with BPF hash maps.
packet arrives at NIC
│
▼
XDP hook — before the kernel stack
│
▼
eBPF program runs in-kernel
│
▼
hash map lookup — O(1)
│
▼
decision: allow / drop / redirect
│
▼
delivered to pod directly
The critical difference is O(1) instead of O(n). A hash map lookup takes the same time whether you have 10 services or 10,000. iptables gets slower as the cluster grows; Cilium’s eBPF path does not.
What Cilium adds on top of the raw mechanism
eBPF is the primitive. Cilium is what turns it into a CNI:
- Network policy — L3/L4/L7 enforcement at the kernel level, evaluated via the same hash-map lookup
- mTLS — transparent encryption between pods without a sidecar proxy
- Hubble — real-time flow observability. When a pod can’t reach something, Hubble shows you exactly which policy dropped the traffic, live
- Load balancing — replaces kube-proxy for Service routing entirely, no iptables rules generated at all
That last point is worth sitting with. Cilium doesn’t optimize iptables. It removes it as a component. There is no rule chain to traverse because there is no rule chain.
A concrete default-deny policy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: payments
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: payment-service-egress
namespace: payments
spec:
podSelector:
matchLabels:
app: payment-service
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- port: 5432
Default deny everything, explicit allow only what’s needed. If the payment service is compromised, lateral movement is structurally prevented — not by convention, but because there is no path for the packet to take.
When iptables is still the right call
For a small cluster with a handful of services, iptables is battle-tested, well understood, and every engineer already knows how to debug it with iptables -L. The eBPF migration is worth the operational change specifically once cluster scale, policy complexity, or observability requirements make the O(n) traversal a real cost rather than a theoretical one.
The pattern to remember: iptables is a static rule chain. eBPF is a programmable kernel layer. One gets slower as you grow. The other doesn’t.