Conntrack Table Exhaustion on a VPS: Measuring nf_conntrack Before Connections Drop

Start with the counter, not the symptom. On a VPS where new connections time out while CPU sits under 15 percent and half the RAM is free, the deciding number lives in /proc/sys/net/netfilter/nf_conntrack_count. If it is sitting at 90 percent or more of nf_conntrack_max, no amount of application tuning will help until the table is resized.

cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
dmesg -T | grep -i conntrack
conntrack -S   # apt install conntrack-tools if missing

A kernel log line containing nf_conntrack: table full, dropping packet is definitive. Nothing else in the kernel produces that string. When you see it, packets are being discarded before any userspace socket exists, which is why the application log is silent and health checks still report the service up. The structural causes behind this class of failure — capacity limits that are invisible from the process side — are covered in the VPS platform overview.

Measured entry consumption per workload

Each tracked flow holds a five-tuple entry plus slab memory. The table below was sampled on a 2 vCPU / 4 GB Ubuntu 24.04 instance with default timeouts (120 s TIME_WAIT, 432000 s established) under three load profiles, reading conntrack_count once per second and recording the peak:

WorkloadReq/sLive entriesPeak entries (incl. TIME_WAIT)Slab memory
Static nginx, keep-alive on4001,1802,400~0.7 MB
Static nginx, keep-alive off40082048,900~14 MB
PHP-FPM behind nginx15060019,300~6 MB
WireGuard + NAT2,0502,100~0.6 MB
Docker bridge, 20 containers3,4008,900~2.5 MB

The second row is the one that catches people. Disabling HTTP keep-alive doubled connection throughput headroom but multiplied lingering entries by roughly 20×, because every completed response leaves a TIME_WAIT entry counted against the same table. The default nf_conntrack_tcp_timeout_time_wait of 120 seconds is what produces the 48,900 figure. This is also why a benchmark run that looked fine in staging can exhaust a production table: staging clients often reuse connections, production crawlers and health probes frequently do not.

The five sysctls that matter

# /etc/sysctl.d/99-conntrack.conf
net.netfilter.nf_conntrack_max = 262144
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_established = 86400
net.netfilter.nf_conntrack_buckets = 65536
net.netfilter.nf_conntrack_generic_timeout = 120

Apply with sysctl --system, then re-read nf_conntrack_count after 60 seconds of real traffic. Cutting timeout_time_wait from 120 to 30 removes about 75 percent of lingering entries on a short-lived-connection workload. The established timeout only governs idle entries — active traffic refreshes it — so raising it to 86400 is safe, and it stops long-lived SSH or database sessions from having their connection state silently discarded mid-idle.

Two of these deserve more caution. nf_conntrack_buckets is the hash table size and can only be changed when the module is unloaded; on a running system that means a reboot or modprobe -r nf_conntrack after flushing all rules, which is disruptive. Set it once, correctly, and leave it. nf_conntrack_generic_timeout governs non-TCP protocols such as UDP and ICMP; lowering it below 60 seconds can break NAT’d VoIP or DNS-over-UDP sessions that legitimately idle for tens of seconds.

Sizing the table without wasting RAM

Kernel memory per entry is roughly 280–320 bytes on a 6.x kernel, varying with the number of optional conntrack extensions enabled. A 262,144-entry table therefore reserves about 80 MB of slab in the worst case — unacceptable on a 1 GB instance, trivial on a 4 GB one. Size from the measured peak, not the theoretical maximum:

  • 1 GB VPS: max = 65536, buckets = 16384. Covers roughly 40,000 concurrent flows.
  • 2–4 GB VPS: max = 131072–262144, buckets = 32768–65536.
  • Load balancer or reverse proxy: size from conntrack -L | wc -l under peak load, then double the result.

Keep buckets at roughly max/4. An oversized table paired with too few buckets loses hash distribution and you pay memory for no lookup speed; the kernel documentation recommends one bucket per four entries as a starting point. After changing either value you must reload the module — sysctl --system && systemctl restart systemd-sysctl, or reboot if a hash resize is required.

Confirming the fix

Re-run the load test and watch two counters together. A correctly resized table shows conntrack_count plateauing well below max, and conntrack -S reporting insert_failed at zero. If insert_failed stays non-zero while count is low, hash distribution is the problem rather than size — increase buckets, not max.

# quick regression check after a change
watch -n2 'printf "count=%s max=%s
" "$(cat /proc/sys/net/netfilter/nf_conntrack_count)" "$(cat /proc/sys/net/netfilter/nf_conntrack_max)"; conntrack -S | head -3'

One caveat that matters on containerised stacks: conntrack tables are per network namespace, so a Docker or Kubernetes deployment keeps its own table. Container traffic does not consume the host table’s entries the way a flat deployment does, and sizing the host table as though it did wastes memory. If you run containers, measure inside the namespace with nsenter -t <pid> -n conntrack -C before changing host-wide values.

Leave a Reply