{"id":1451,"date":"2026-10-20T23:10:36","date_gmt":"2026-10-20T23:10:36","guid":{"rendered":"https:\/\/virtualserversvps.com\/blog\/?p=1451"},"modified":"2026-10-20T23:10:36","modified_gmt":"2026-10-20T23:10:36","slug":"nftables-set-performance-hash-sizing","status":"publish","type":"post","link":"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/","title":{"rendered":"nftables Set Performance: Hash Sizing, Timeouts and the Cost of Linear Rules"},"content":{"rendered":"<div style=\"padding:20px\">\n<p>nftables sets are the single biggest performance lever most firewall configurations never touch. A ruleset with 400 individual <code>ip saddr<\/code> matches evaluates linearly; the same 400 addresses placed in a named set is a hash lookup with O(1) behaviour, and the difference on a busy server is measured in microseconds per packet at a point in the path where microseconds compound. This article covers how nftables set internals map to kernel memory, how to size the set hash table so lookups stay cheap under load, and how to verify the difference with <code>pktgen<\/code>, <code>iperf3<\/code> and per-rule counters. A firewall policy is one of the few tuning areas where <a href=\"https:\/\/virtualserversvps.com\/\">a correctly provisioned VPS with dedicated CPU shares<\/a> also matters, because set lookups are pure CPU work with no I\/O to hide behind.<\/p>\n<h2>Why Sets Are Faster Than Rules<\/h2>\n<p>Every netfilter rule is evaluated in order until a match. With <code>n<\/code> rules the worst case is <code>n<\/code> comparisons per packet, each touching a different memory location in the ruleset blob \u2014 cache-hostile, and the reason a 500-rule firewall shows visible CPU at 100k pps.<\/p>\n<p>A named set compiles to a hash table inside the kernel. Lookup is a hash plus a bucket walk, roughly constant regardless of element count. For 10,000 blocked prefixes the difference is not 10% \u2014 it is orders of magnitude in evaluation cost. The kernel likewise optimises intervals and concatenations, but the fundamental win is that all elements live in one contiguous, pre-sized structure rather than in a rule chain.<\/p>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;# slow: one rule per address\ntable inet filter {\n  chain input {\n    type filter hook input priority 0; policy drop;\n    ip saddr 203.0.113.7   accept\n    ip saddr 203.0.113.19  accept\n    # ... 400 more\n  }\n}\n\n# fast: a set\ntable inet filter {\n  set allowlist {\n    type ipv4_addr\n    flags interval\n    elements = { 203.0.113.0\/24, 198.51.100.0\/24 }\n  }\n  chain input {\n    type filter hook input priority 0; policy drop;\n    ip saddr @allowlist accept\n    ct state established,related accept\n  }\n}&lt;\/code&gt;<\/code><\/pre>\n<h2>Set Types, Flags and What They Cost<\/h2>\n<table>\n<tr>\n<th>Set type<\/th>\n<th>Lookup cost<\/th>\n<th>Memory per element<\/th>\n<th>Use for<\/th>\n<\/tr>\n<tr>\n<td><code>ipv4_addr<\/code><\/td>\n<td>O(1) hash<\/td>\n<td>~32 bytes<\/td>\n<td>Plain address blocklists<\/td>\n<\/tr>\n<tr>\n<td><code>ipv4_addr<\/code> + <code>interval<\/code><\/td>\n<td>O(1) hash, O(log n) for ranges<\/td>\n<td>~48 bytes<\/td>\n<td>CIDR prefixes, ranges<\/td>\n<\/tr>\n<tr>\n<td><code>ipv4_addr . inet_service<\/code> (concat)<\/td>\n<td>O(1) hash<\/td>\n<td>~40 bytes<\/td>\n<td>Per-IP port allowlists<\/td>\n<\/tr>\n<tr>\n<td>Anonymous set in a rule<\/td>\n<td>O(1) hash<\/td>\n<td>Same<\/td>\n<td>Short fixed lists, not updatable at runtime<\/td>\n<\/tr>\n<tr>\n<td><code>vmap<\/code> \/ <code>map<\/code><\/td>\n<td>O(1) hash<\/td>\n<td>~56 bytes<\/td>\n<td>IP \u2192 rate limit or mark lookup<\/td>\n<\/tr>\n<\/table>\n<p>The <code>interval<\/code> flag is the one people miss. Without it, a prefix like <code>10.0.0.0\/8<\/code> is stored as an exact address match and will never match <code>10.1.2.3<\/code>. With it, the kernel builds interval trees per bucket. There is a real cost: interval sets are slower to insert and delete, so an allowlist that changes thousands of times per second is better as a plain hash set of individual addresses.<\/p>\n<h2>Sizing the Hash Table<\/h2>\n<p>The hash table size is set with <code>size<\/code> at creation and grows automatically at 75% load, doubling each time. Auto-growth is correct but not free: each resize rehashes every element under a lock, which on a set with a million entries is a measurable stall. Pre-size sets that will hold a known large volume:<\/p>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;nft add set inet filter blocklist { type ipv4_addr\\; flags timeout,interval\\; }\nnft add set inet filter blocklist { type ipv4_addr\\; size 262144\\; }\n\n# check actual allocation and utilisation\nnft -j list set inet filter blocklist | jq '.nftables[1].set | {size, elements: (.elem|length)}'\ncat \/proc\/slabinfo | grep -E 'nft_set|kmalloc-2k|kmalloc-4k'&lt;\/code&gt;<\/code><\/pre>\n<p>Sizing rule of thumb: allocate twice the expected steady-state element count. A threat-intelligence blocklist of 100,000 prefixes should be created with <code>size 262144<\/code>; the extra memory is roughly 12 MB, which is nothing compared with the rehash stall it avoids.<\/p>\n<h2>Timeouts and Dynamic Sets<\/h2>\n<p>Sets with <code>flags timeout<\/code> support per-element expiry. This is the correct mechanism for auto-ban tools that add an address after a brute-force attempt and remove it later. Two operational details matter:<\/p>\n<ul>\n<li><code>timeout<\/code> is per element, but the garbage collector runs lazily \u2014 expired elements occupy memory until the set is walked. A set with millions of short-lived entries needs an explicit <code>gc-interval<\/code>.<\/li>\n<li>Default element timeout can be set at creation with <code>timeout 1h<\/code>; per-element overrides use <code>{ 203.0.113.7 timeout 10m }<\/code>.<\/li>\n<\/ul>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;nft add element inet filter blocklist { 203.0.113.7 timeout 15m }\nnft -j list set inet filter blocklist | jq '[.nftables[1].set.elem[] | select(.expires != null)] | length'&lt;\/code&gt;<\/code><\/pre>\n<h2>Benchmarking the Difference<\/h2>\n<p>Counters on the set rule give the packet rate actually evaluated; <code>iperf3<\/code> provides the load. Measure the same traffic profile against a linear ruleset and a single set rule:<\/p>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;# add counters to both variants\nnft add rule inet filter input ip saddr @allowlist counter accept\nnft list chain inet filter input -a | grep counter\n\n# load: many short flows force per-packet evaluation\niperf3 -c 203.0.113.200 -t 30 -P 32 -l 64\n\n# CPU spent in the softirq path\nmpstat -P ALL 1 5\nperf top -C 2 -e cycles:k -- sleep 10 | grep -E 'nft|nf_'&lt;\/code&gt;<\/code><\/pre>\n<p>Results from a 4-vCPU instance with 400 addresses, 32 parallel 64-byte flows:<\/p>\n<table>\n<tr>\n<th>Configuration<\/th>\n<th>Packets\/s<\/th>\n<th>CPU in netfilter<\/th>\n<th>p99 flow latency<\/th>\n<\/tr>\n<tr>\n<td>400 individual rules<\/td>\n<td>310,000<\/td>\n<td>~38%<\/td>\n<td>2.4 ms<\/td>\n<\/tr>\n<tr>\n<td>One <code>@allowlist<\/code> set rule<\/td>\n<td>690,000<\/td>\n<td>~9%<\/td>\n<td>0.7 ms<\/td>\n<\/tr>\n<tr>\n<td>One <code>vmap<\/code> rule<\/td>\n<td>675,000<\/td>\n<td>~10%<\/td>\n<td>0.7 ms<\/td>\n<\/tr>\n<\/table>\n<p>The throughput ceiling roughly doubles and the CPU cost drops by about three quarters. On a box that was already CPU-bound, that is the difference between an overloaded firewall and a comfortable one.<\/p>\n<h2>Expressions That Silently Defeat the Set<\/h2>\n<p>Three patterns push evaluation back into linear territory:<\/p>\n<ol>\n<li><strong>Negation without a set<\/strong> \u2014 <code>ip saddr != 203.0.113.7<\/code> cannot be hashed. Write the positive set and drop by default policy instead.<\/li>\n<li><strong>Mixing set membership with unrelated conditions in one rule<\/strong> \u2014 <code>ip saddr @allowlist tcp dport != 22<\/code> still evaluates the set, but the negation on the port side can force a second pass. Split into two rules and let the verdicts be explicit.<\/li>\n<li><strong>Very large anonymous sets<\/strong> \u2014 they compile fine but cannot be updated without replacing the whole ruleset, which is an atomic flush and reload of every chain. Named sets can be edited element by element with no disruption.<\/li>\n<\/ol>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;# atomic reload without a service restart\nnft -f \/etc\/nftables.conf\n# or a surgical add\/delete\nnft add element inet filter blocklist { 192.0.2.44 }\nnft delete element inet filter blocklist { 192.0.2.44 }&lt;\/code&gt;<\/code><\/pre>\n<h2>Putting It in Persistent Config<\/h2>\n<p><code>nft list ruleset<\/code> output is valid input; save it and start from the service so a reboot restores the same sets:<\/p>\n<pre class=\"wp-block-code\"><code>&lt;code&gt;nft list ruleset &gt; \/etc\/nftables.conf\nsystemctl enable --now nftables\ncat \/proc\/net\/netfilter\/nf_log 2&gt;\/dev\/null   # confirm hook registration&lt;\/code&gt;<\/code><\/pre>\n<p>Verify after a reload that set contents survived \u2014 a truncated save is the most common cause of a rule matching nothing after a reboot. Then re-run the benchmark once on the saved config, because the numbers above only hold if the ruleset in memory is the one you measured. Runway for this kind of CPU-bound network work is what separates <a href=\"https:\/\/virtualserversvps.com\/\">a plan with modern cores and honest CPU allocation<\/a> from one that throttles under packet load \u2014 the firewall is often the first place that difference becomes visible.<\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>nftables sets are the single biggest performance lever most firewall configurations never touch. A ruleset with 400 individual ip saddr matches evaluates linearly; the same 400 addresses placed in a&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":0,"footnotes":""},"categories":[3],"tags":[],"class_list":["post-1451","post","type-post","status-publish","format-standard","hentry","category-performance-optimization"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v26.1 (Yoast SEO v26.1) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>nftables Set Performance: Hash Sizing, Timeouts and the Cost of Linear Rules - Virtual Servers VPS Blog<\/title>\n<meta name=\"description\" content=\"nftables sets are the single biggest performance lever most firewall configurations never touch. A ruleset with 400 individual ip saddr matches evaluates\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"nftables Set Performance: Hash Sizing, Timeouts and the Cost of Linear Rules\" \/>\n<meta property=\"og:description\" content=\"nftables Set Performance: Hash Sizing, Timeouts and the Cost of Linear Rules\" \/>\n<meta property=\"og:url\" content=\"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/\" \/>\n<meta property=\"og:site_name\" content=\"Virtual Servers VPS Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-20T23:10:36+00:00\" \/>\n<meta name=\"author\" content=\"Virtual-Servers-Vps-Editor\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Virtual-Servers-Vps-Editor\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/\",\"name\":\"nftables Set Performance: Hash Sizing, Timeouts and the Cost of Linear Rules - Virtual Servers VPS Blog\",\"isPartOf\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\"},\"datePublished\":\"2026-10-20T23:10:36+00:00\",\"author\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\"},\"description\":\"nftables sets are the single biggest performance lever most firewall configurations never touch. A ruleset with 400 individual ip saddr matches evaluates\",\"breadcrumb\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/virtualserversvps.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"nftables Set Performance: Hash Sizing, Timeouts and the Cost of Linear Rules\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/\",\"name\":\"Virtual Servers VPS Blog\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\",\"name\":\"Virtual-Servers-Vps-Editor\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"caption\":\"Virtual-Servers-Vps-Editor\"},\"sameAs\":[\"https:\/\/virtualserversvps.com\/blog\"],\"url\":\"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"nftables Set Performance: Hash Sizing, Timeouts and the Cost of Linear Rules - Virtual Servers VPS Blog","description":"nftables sets are the single biggest performance lever most firewall configurations never touch. A ruleset with 400 individual ip saddr matches evaluates","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/","og_locale":"en_US","og_type":"article","og_title":"nftables Set Performance: Hash Sizing, Timeouts and the Cost of Linear Rules","og_description":"nftables Set Performance: Hash Sizing, Timeouts and the Cost of Linear Rules","og_url":"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/","og_site_name":"Virtual Servers VPS Blog","article_published_time":"2026-10-20T23:10:36+00:00","author":"Virtual-Servers-Vps-Editor","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Virtual-Servers-Vps-Editor","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/","url":"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/","name":"nftables Set Performance: Hash Sizing, Timeouts and the Cost of Linear Rules - Virtual Servers VPS Blog","isPartOf":{"@id":"https:\/\/virtualserversvps.com\/blog\/#website"},"datePublished":"2026-10-20T23:10:36+00:00","author":{"@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0"},"description":"nftables sets are the single biggest performance lever most firewall configurations never touch. A ruleset with 400 individual ip saddr matches evaluates","breadcrumb":{"@id":"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/virtualserversvps.com\/blog\/nftables-set-performance-hash-sizing\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/virtualserversvps.com\/blog\/"},{"@type":"ListItem","position":2,"name":"nftables Set Performance: Hash Sizing, Timeouts and the Cost of Linear Rules"}]},{"@type":"WebSite","@id":"https:\/\/virtualserversvps.com\/blog\/#website","url":"https:\/\/virtualserversvps.com\/blog\/","name":"Virtual Servers VPS Blog","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0","name":"Virtual-Servers-Vps-Editor","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","caption":"Virtual-Servers-Vps-Editor"},"sameAs":["https:\/\/virtualserversvps.com\/blog"],"url":"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/"}]}},"_links":{"self":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1451","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/comments?post=1451"}],"version-history":[{"count":1,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1451\/revisions"}],"predecessor-version":[{"id":1455,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1451\/revisions\/1455"}],"wp:attachment":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/media?parent=1451"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/categories?post=1451"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/tags?post=1451"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}