ipset Memory Footprint: Sizing Large Blocklists on a Small VPS

An ipset holding a million IPv4 /32 entries consumes roughly 100 MB of kernel memory in the default configuration — and that number is not fixed by the entry count alone. It depends on the set type, the hash bucket size, the maxelem value and whether the set has timeout support enabled. On a 2 GB VPS, a careless hash:net set can push the box into swap and take sshd with it, which makes the sizing decision a reliability question, not an optimisation.

The arithmetic is simple enough to do before deploying. Each element costs one hash node structure plus the stored key plus a bucket chain pointer, and the table allocates ahead of need based on maxelem. This article gives measured per-100k costs for the set types people actually use for blocklists, how maxelem and hashsize interact, and when to switch from an ipset to a userspace validator. All of it matters most on a small, memory-bound virtual server where 60 MB is the difference between stable and swapping.

What one element actually costs

For hash:ip, the kernel stores a 16-byte ip_set_ext region when extensions are off, and the hash node overhead brings it to roughly 40 bytes per element on x86_64. With counter or timeout extensions enabled, the extension block balloons to about 64 bytes, and the effective per-entry cost rises to somewhere between 85 and 100 bytes. That is a 2x swing from a flag you may have set without noticing.

Set typeNo extensionsWith timeout/counter
hash:ip~40 B~90 B
hash:net (aggregated)~45 B~95 B
hash:net (random /24s)~85 B~140 B
hash:ip,port~70 B~130 B

Why hash:net with random prefixes costs the most

hash:net stores each prefix length in its own table named hash:net.N when you use a mixed-prefix set, and each table has its own bucket array allocated at creation time. A blocklist with /24s scattered across the full IPv4 space plus a handful of /8s ends up with nine or ten shadow tables, each of which pre-allocates buckets. The element cost is the same, but the empty table overhead is real: around 40 KB per table with default sizing, multiplied by bucket count.

Aggregating contiguous entries is the single biggest win. A list of 250,000 /32s drawn from 4,000 adjacent /24s collapses to 4,000 /24 entries plus a few boundary /32s, dropping memory from about 20 MB to under 400 KB and lookup cost with it. ipset restore does not merge anything for you; the aggregation has to happen in whatever builds the list.

maxelem is a ceiling, and it is charged upfront

When you create a set with maxelem 1000000, the kernel sizes the bucket array to keep the average chain short, and that array is allocated immediately. A set created for a million entries but loaded with 50,000 holds memory for the ceiling, not the contents. The practical rule is to set maxelem to 1.5x your measured peak entry count, not to a round number that looks comfortable.

ipset create bl4 hash:net family inet maxelem 300000 hashsize 65536 timeout 86400 counters
ipset create bl4v6 hash:net family inet6 maxelem 50000  hashsize 8192

# Check real kernel allocation
ipset list bl4 | head -8
grep ip_set /proc/slabinfo

The hashsize hint matters too. Too small and chains lengthen, raising lookup time and collision handling; too large and you pay for buckets that never fill. For a load factor near 1.0, choose hashsize roughly equal to maxelem divided by four and rounded to a power of two. Going beyond that buys nothing measurable in lookup time.

The 100k-entry baseline, measured

The numbers below come from a DHCP-free test on a 2 GB KVM guest running a 6.x kernel with CONFIG_IP_SET_MAX at its default of 256 sets. They are close enough to plan capacity with. Note that per-100k cost with timeouts enabled is nearly double the plain case, which is why the blocklist refresh strategy should favour full replacement over per-entry timeouts.

ConfigurationPer 100kPer 1M
hash:ip, no ext~4 MB~40 MB
hash:ip, timeout+counters~9 MB~90 MB
hash:net, /24 aggregated~4.5 MB~45 MB
hash:net, random /32s~8.5 MB~85 MB
hash:ip,port~13 MB~130 MB

Budgeting against total VPS RAM

Keep ipsets under a quarter of available RAM. On a 1 GB VPS that is 256 MB, but the OS, the packet path and the firewall rule counters take their own share, so a realistic ceiling is about 150 MB of set data. Beyond that point you start losing page cache and the box spends time reclaiming rather than forwarding packets.

  • 1 GB VPS: stay under 40 MB of sets, roughly 800k plain /32s
  • 2 GB VPS: stay under 90 MB, roughly 2M plain /32s
  • 4 GB VPS: stay under 180 MB, roughly 4M plain /32s with aggregation
  • Any size: reserve 8 percent for kernel slab and set metadata overhead

Operating limits you will hit before memory

Two ceilings bite sooner than RAM on busy boxes. The default maximum number of elements per set is one million, configurable at module load with maxelem up to 2^32. The bucket limit and ip_set_max are also finite. If ipset add returns “element already added” during a refresh, you are colliding with an existing set, not exhausting memory.

Hot updates matter more than raw size. Rebuilding a 90 MB set with ipset swap doubles peak memory for the duration of the swap, so plan for 2x during refresh or accept a brief window where the new set is not yet active. Using ipset restore -exist against the live set avoids the doubling but holds stale entries until they are explicitly removed.

When to leave the kernel behind

Above roughly five million entries or when the list needs frequent per-entry expiry, the ipset approach becomes awkward. A userspace validator with a compressed trie or a sorted array plus binary search can hold ten million prefixes in under 40 MB and reload atomically without kernel swap windows. The tradeoff is that it adds a lookup step per new connection.

For most single-server blocklists, the kernel set at 500k to 2M entries is still the faster and simpler design. Measure with slabinfo after a full load rather than trusting a formula, and re-check after any kernel upgrade, since structure sizes have changed between major versions. On a properly provisioned KVM server the difference between a 4 MB and a 90 MB set is the difference between headroom and an OOM kill.

Leave a Reply