
iptables vs nftables: What Changed and How to Migrate
If your server runs a current Debian, Ubuntu, or RHEL-family distro, your "iptables" rules are almost certainly already executing inside nftables — the iptables command has been a compatibility shim since 2019. The real question isn't which one wins; it's whether to keep writing rules in a legacy syntax on top of the new engine. This guide covers what actually changed architecturally, a side-by-side syntax comparison, what the honest performance data says, and a migration path that won't break Docker or lock you out.
08 juillet 2026
par Jesse Schokker
nftables
iptables
Firewall
Linux
Loading...
Answer first: you're probably already running nftables
The framing "iptables vs nftables" suggests a choice between two live options. For most servers in 2026 that choice was already made by your distribution:
- On Debian 10+ and Ubuntu 20.10+, the
iptablescommand is by defaultiptables-nft— a compatibility layer that accepts iptables syntax but programs the kernel's nf_tables subsystem. Your iptables rules already run on the nftables engine. - On the RHEL family (RHEL/AlmaLinux/Rocky 9 and 10), the whole iptables framework is deprecated, firewalld speaks nftables natively, and RHEL 10 no longer ships the legacy
ip_tableskernel module at all. - Upstream, iptables is in maintenance mode — still receiving releases (1.8.13 landed in March 2026), but all feature development happens in nftables.
So the practical guidance is short: existing, working iptables rulesets are not an emergency — the compatibility layer is good and stable. But new rules and new automation should be written in native nftables: one syntax for IPv4 and IPv6, sets and maps that scale, atomic reloads, and no dependence on a shim whose underlying tooling distros have marked for eventual removal. The rest of this article is the detail behind that advice, and the migration mechanics.
How we got here
netfilter — the kernel's packet-filtering subsystem — has been through three user-facing generations: ipchains (kernel 2.2), iptables (kernel 2.4, 2001), and nftables (merged in kernel 3.13, 2014). nftables was written by the same netfilter team to fix iptables' structural problems rather than patch around them.
The pivotal release was iptables 1.8 (2018), which split the command into two variants: iptables-legacy (the classic implementation) and iptables-nft (same command-line syntax, but rules are executed by the nf_tables kernel engine). Distros switched their default alias to the nft variant almost immediately, which is how millions of servers migrated engines without their operators noticing:
| Distro | Status |
|---|---|
| Debian 10 → 13 | iptables = iptables-nft by default since 2019; nftables is the recommended framework |
| Ubuntu 20.10 → 26.04 LTS | Same — iptables-nft default; ufw still drives iptables syntax (over the shim) |
| RHEL 8 | firewalld gains nftables backend; iptables present |
| RHEL/Alma/Rocky 9 | Entire iptables framework deprecated; firewalld/nftables is the supported path |
| RHEL/Alma/Rocky 10 (2025) | Legacy ip_tables kernel module removed; iptables-nft userspace still ships, deprecated |
Check what any given box is actually using — the answer is printed in parentheses:
$ iptables -V
iptables v1.8.10 (nf_tables) # ← the shim, executing on nftables
iptables v1.8.10 (legacy) # ← the classic engine
What actually changed
The differences that matter operationally, not the marketing list:
One tool, both address families
iptables' original sin: iptables, ip6tables, arptables, and ebtables were four separate tools with four separate rulesets, and every dual-stack server needed its policy written (and maintained, and audited) twice. Forgetting the IPv6 half was the classic hole. nftables' inet family applies one table to both IPv4 and IPv6 — the entire class of "firewalled v4, forgot v6" bugs disappears structurally.
Sets and maps are first-class
In iptables, matching 500 addresses meant 500 rules (evaluated linearly) or bolting on the separate ipset tool. In nftables, sets are native:
nft add set inet filter blocklist '{ type ipv4_addr; flags interval; }'
nft add element inet filter blocklist '{ 203.0.113.0/24, 198.51.100.7 }'
nft add rule inet filter input ip saddr @blocklist drop
One rule, O(1)-ish lookup, updatable at runtime without touching the ruleset. Maps go further — mapping match criteria to actions or destinations (dnat to tcp dport map { 80 : 10.0.0.10, 443 : 10.0.0.11 }-style constructs collapse whole rule families into a lookup). Dynamic sets also power per-source rate limiting, which we use in our default firewall rules baseline.
Atomic ruleset replacement
nft -f ruleset.conf validates and applies the entire file atomically — it either fully loads or the old ruleset stays untouched. iptables scripts applied rules one command at a time, so a failure halfway left the firewall in a half-configured state (occasionally, memorably, in the state where the default-drop was applied and the SSH allow wasn't). iptables-restore mitigated this per-table; nftables makes it the only mode of operation. Plus nft -c gives you a dry-run syntax check.
No built-in chains, no fixed pipeline
iptables gave every table fixed chains (INPUT, FORWARD, OUTPUT...) that exist whether used or not, each imposing per-packet cost. In nftables, you create only the chains you need and attach them to kernel hooks with explicit priorities. Empty-firewall overhead is genuinely zero, and the ruleset you read is the whole truth — nothing implicit.
Better introspection and automation
nft monitor streams ruleset changes live (superb for "what just changed my firewall?" debugging), counters are opt-in per rule, and nft -j speaks JSON both ways — the modern answer to a generation of brittle iptables-save parsers. If you template firewall config with Ansible or Terraform, native nftables is markedly less painful.
Syntax side by side
The tasks you actually do, in both languages:
| Task | iptables | nftables |
|---|---|---|
| List rules | iptables -L -n -v | nft list ruleset |
| Allow a port | iptables -A INPUT -p tcp --dport 443 -j ACCEPT | nft add rule inet filter input tcp dport 443 accept |
| Several ports | -m multiport --dports 80,443,8443 | tcp dport { 80, 443, 8443 } accept |
| Stateful accept | -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT | ct state established,related accept |
| Both v4 + v6 | run iptables and ip6tables | one inet table covers both |
| Match many IPs | ipset create + -m set --match-set | native set: ip saddr @blocklist drop |
| Log then drop | two rules (-j LOG, then -j DROP) | one rule: log prefix "drop: " drop |
| Rate limit | -m limit --limit 10/minute | limit rate 10/minute |
| Save / restore | iptables-save / iptables-restore (per family) | nft list ruleset > f / nft -f f (atomic, both families) |
| Dry-run check | — | nft -c -f ruleset.conf |
The nftables column reads like a language rather than a command's flag soup — because it is one: expressions combine freely (ip saddr @admins tcp dport 22 ct state new accept) instead of requiring a bespoke match module per feature.
Performance: the honest version
Claims that nftables is dramatically faster (or slower) are both easy to find and mostly unsupported. The credible public data:
- Red Hat's netfilter developers benchmarked the two in 2017: with small, linear rulesets iptables was actually slightly faster; the picture inverts as soon as sets enter — matching across ~120+ ports or large address groups, iptables degrades linearly while nftables stays flat. Their conclusion was that the architectures are comparable and the win is sets/maps, not raw per-rule speed. (2017 data — the shape of the conclusion has held, but treat the absolute numbers as historical.)
- At orchestration scale the difference becomes visible: Kubernetes' kube-proxy gained a GA nftables mode in 1.33 primarily because iptables' linear rule evaluation and full-table reloads hurt at tens of thousands of services — at ~30,000 services, nftables-mode tail latency beat iptables-mode's best latency. Notably, iptables mode remains Kubernetes' default for compatibility.
For a single server with dozens of rules, you will measure no difference. Choose nftables for the expressiveness, atomicity, and the fact that it's where all maintenance and development effort now goes — not for throughput.
Migrating
Step 1: find out what you're running
iptables -V (see above). Also check for rules in both engines — a surprisingly common mess after partial migrations:
iptables-legacy -L -n 2>/dev/null # anything here runs on the old engine
nft list ruleset # the full truth, including iptables-nft rules
nft list ruleset shows everything in nf_tables, including rules created through the shim — it's the one command that never lies to you about the firewall's real state.
Step 2: mechanical translation
The iptables package ships translators — same syntax in, nft syntax out:
$ iptables-translate -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
nft 'add rule ip filter INPUT tcp dport 22 ct state new counter accept'
# whole ruleset at once:
iptables-save > current.rules
iptables-restore-translate -f current.rules > ruleset.nft
nft -c -f ruleset.nft # syntax-check before applying
(ip6tables-restore-translate for the v6 half.) The output is correct but literal — it reproduces your v4/v6 duplication and per-rule structure as-is.
Step 3: actually adopt the model
Treat the translated file as scaffolding, then restructure: merge the v4/v6 rulesets into one inet table, collapse repeated port/address rules into sets, and add comments. A 100-line iptables ruleset routinely becomes 30 readable nftables lines. Our firewall baseline article is a reasonable skeleton to migrate onto. Apply with a scheduled rollback and an open second SSH session (lockout-avoidance mechanics are covered there too).
The pitfalls
- Don't run rules in both engines. Legacy iptables and nftables rules are both evaluated, independently — a packet must survive both. This "works" until it produces undebuggable behaviour. Migrate, verify, then flush the legacy side.
- Docker still speaks iptables. Docker programs its NAT/forwarding through the iptables interface by default (fine — on modern systems that's the nft shim; don't remove the
iptablespackage on a Docker host). Its native nftables backend shipped experimentally in Engine 29 but is opt-in. Practical consequence: on a Docker host, put your policy in theDOCKER-USERchain or in your own tables, and don'tflush rulesetblindly — that nukes Docker's rules too. (Same story for Kubernetes nodes: check your CNI and kube-proxy mode before touching the firewall.) - Frontends are fine — pick one layer. ufw (iptables syntax over the shim) and firewalld (native nftables backend since 2018) both work correctly today. The anti-pattern is editing nft tables by hand on a machine a frontend manages; the frontend will win on the next reload.
When staying on iptables is reasonable
Honesty about the other side: a stable appliance-like server with a working, audited iptables-nft ruleset and no active rule development has little to gain from a rewrite. Tooling that only emits iptables (older fail2ban actions, legacy orchestration) is also a legitimate reason to defer — the shim exists precisely for this. The line to hold is directional: don't write new systems, roles, or documentation against the legacy syntax, because the RHEL family has already scheduled its removal and the rest of the ecosystem follows Red Hat's netfilter team.
Frequently asked questions
Is iptables deprecated?
Precisely: deprecated by distributions, maintained upstream. The netfilter project still releases iptables (1.8.13, March 2026) and has published no end-of-life. But Red Hat deprecated the entire iptables framework in RHEL 9, removed the legacy kernel module in RHEL 10, and every major distro's default has been the nftables engine for years. "Dead" is wrong; "legacy interface on borrowed time" is right.
Do ufw and firewalld use nftables?
firewalld: yes, natively — nftables has been its default backend since 2018. ufw: indirectly — it generates iptables rules and executes them through the system's iptables binary, which on any modern Ubuntu is the nft-based shim, so the rules land in nf_tables; ufw itself has no native nftables mode. Either way, your rules end up in the same kernel subsystem.
Will my iptables rules stop working if I switch?
Your rules' logic translates cleanly — iptables-restore-translate converts a saved ruleset mechanically, and the nftables engine supports everything the baseline iptables feature set does. What doesn't carry over automatically: ipset definitions (recreate as native sets), any script that parses iptables -L output, and assumptions about built-in chains. Translate, dry-run with nft -c, apply atomically, keep a rollback scheduled.
Is nftables faster than iptables?
At small scale, no — Red Hat's own benchmarks found plain linear rulesets marginally favoured iptables. nftables wins where structure matters: large port/address sets (flat instead of linear lookup), very large rulesets, and frequent rule churn (atomic incremental updates instead of full-table reloads — the reason Kubernetes built an nftables kube-proxy). For a typical server firewall, choose on maintainability; performance is a wash.
Deploying on Serverside
Whichever syntax you write, the on-host firewall is one layer of two on our network: every dedicated server includes a self-service edge firewall — non-service traffic is dropped upstream before it reaches your port — plus always-on DDoS mitigation on ASN 55285, with sub-minute provisioning on any major Linux distribution.
Build on this article with the complete default firewall ruleset for a Linux server, the bigger threat picture in DDoS attacks explained, and hands-on traffic forensics in capturing and analysing a DDoS attack with Wireshark.

À propos de l'auteur
Jesse SchokkerCo-founder & CTO, Serverside.com
Jesse is the co-founder and CTO of Serverside.com, where he leads the engineering behind the company's bare-metal cloud — from the ASN 55285 backbone to sub-minute server provisioning. He writes about dedicated servers, operating systems, and running production workloads on bare metal.
