Why Tune the Linux Network Stack

The Linux kernel’s default network parameters are designed for general-purpose scenarios — conservative and safe. However, under high-concurrency web services, large-scale load balancers, CDN nodes, and similar workloads, these defaults become bottlenecks. Typical symptoms include:

  • dmesg showing nf_conntrack: table full, dropping packet
  • ss -s showing a large number of TIME-WAIT connections
  • During load testing, CPU is not saturated but throughput plateaus
  • NIC PPS (packets per second) is far below hardware capability

Most of these issues stem from untuned kernel network parameters. This article provides a systematic explanation across four layers: protocol stack parameters, conntrack, NIC multi-queue, and congestion control.

For the complete kernel network parameter documentation, see the kernel.org documentation. The parameters covered here are defined in detail in Documentation/networking/ip-sysctl.txt.

TCP/IP Stack Key Kernel Parameters

All parameters are viewed and modified via sysctl or /proc/sys/net/. The following parameters are all under the net.ipv4 namespace.

Connection Queue Parameters

# View current values
sysctl net.ipv4.tcp_max_syn_backlog
sysctl net.core.somaxconn
sysctl net.core.netdev_max_backlog

somaxconn — The accept queue limit for listening sockets:

# Default is typically 128 or 4096 (depending on kernel version)
# For high concurrency, recommend setting to 65535
net.core.somaxconn = 65535

The accept queue holds connections that have completed the three-way handshake but have not yet been picked up by accept(). When the queue is full, new connections are dropped, and the client sees a connection timeout. Note: the backlog value from the application’s listen(fd, backlog) cannot exceed somaxconn; the actual value is the minimum of the two.

tcp_max_syn_backlog — The SYN queue limit:

net.ipv4.tcp_max_syn_backlog = 65535

The SYN queue holds connections that have received a SYN but have not yet completed the three-way handshake. When the queue is full, SYN packets are dropped, and the client retries. SYN Flood attacks exploit this by exhausting the SYN queue to deny service.

netdev_max_backlog — The receive queue from NIC to protocol stack:

net.core.netdev_max_backlog = 250000

When the NIC passes packets to the kernel protocol stack via interrupts, if the stack cannot keep up, packets are temporarily held in this queue. When the queue is full, the NIC drops new packets. The default value (1000) is far from sufficient for 10G NICs or high-PPS scenarios.

# View TIME-WAIT connection count
ss -s | grep TIME-WAIT

# Typical output
# TCP:   34567 (estab 8900, closed 24000, orphaned 0, synrecv 0, timewait 24000)

tcp_tw_reuse — Allow reuse of TIME-WAIT connections:

net.ipv4.tcp_tw_reuse = 1

When enabled, new connections can reuse local ports that are in the TIME-WAIT state. This is very effective for scenarios that initiate large numbers of short connections (e.g., clients, proxy servers). Note the distinction: tcp_tw_reuse applies to outbound connections (as a client) and has no effect on inbound connections (as a server).

Do not enable the “sibling parameter” tcp_tw_recycle. This parameter causes massive connection drops in NAT environments (because timestamps are inconsistent across machines behind NAT), and was removed in kernel 4.12. If you see articles online recommending enabling both, they are outdated.

tcp_max_tw_buckets — Maximum number of TIME-WAIT connections:

net.ipv4.tcp_max_tw_buckets = 1048576

When the limit is exceeded, excess TIME-WAIT connections are destroyed directly, and a warning is logged. This value is not “the bigger the better” — each TIME-WAIT connection consumes about 1.5KB of memory; 1 million connections consume roughly 1.5GB.

TCP Keepalive Parameters

# Idle time before keepalive probes (seconds)
net.ipv4.tcp_keepalive_time = 600

# Probe interval (seconds)
net.ipv4.tcp_keepalive_intvl = 30

# Number of failed probes before disconnecting
net.ipv4.tcp_keepalive_probes = 3

The default values (7200 seconds / 75 seconds / 9 times) mean a dead connection takes 2 hours to detect. Production environments should reduce this to within 10 minutes.

TCP Buffer Parameters

# TCP read buffer (min/default/max, in bytes)
net.ipv4.tcp_rmem = 4096 87380 67108864

# TCP write buffer
net.ipv4.tcp_wmem = 4096 65536 67108864

# Network layer receive buffer
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

For high bandwidth-delay product (BDP) links (e.g., transcontinental transmission), increasing the maximum value of tcp_rmem can improve single-connection throughput. But be mindful of memory consumption — each connection’s buffer can reach the configured maximum.

conntrack Table Optimization

conntrack (connection tracking) is a Netfilter module feature that records the state of each network connection. On load balancers, NAT gateways, and firewall hosts, a full conntrack table causes packet drops.

Viewing Status

# Current connection count
cat /proc/sys/net/netfilter/nf_conntrack_count
# Example output: 234567

# Maximum connection count
cat /proc/sys/net/netfilter/nf_conntrack_max
# Example output: 262144

# View conntrack hash table size
cat /proc/sys/net/netfilter/nf_conntrack_buckets
# Example output: 65536

Calculating a Reasonable conntrack_max

The conntrack table size should be set based on memory capacity. Each conntrack record consumes approximately 320 bytes of memory:

# Assuming 64GB RAM, reserve 2GB for conntrack
# 2GB / 320B ≈ 6,991,760

# Formula: nf_conntrack_max = available_memory(bytes) / 320
# But in practice, not all memory will be used for conntrack; estimate 1%-3% of memory

# For a 64GB RAM server
echo 1048576 > /proc/sys/net/netfilter/nf_conntrack_max

Hash Table Size

conntrack uses a hash table to store connection records. The hash table size must be a power of 2, and is recommended to be 1/4 to 1/8 of nf_conntrack_max:

# The hash table size can only be set at module load time, not at runtime
# Pass the parameter when loading the nf_conntrack module

# Temporary setting (requires unloading the module first)
sudo modprobe -r nf_conntrack
sudo modprobe nf_conntrack hashsize=262144

# Permanent setting: create a modprobe config
echo "options nf_conntrack hashsize=262144" | sudo tee /etc/modprobe.d/nf_conntrack.conf

Consequences of a too-small hash table: Hash collisions increase, and lookup efficiency degrades from O(1) to O(n), causing elevated network latency. Rule of thumb: hash table size >= nf_conntrack_max / 4.

conntrack Timeout Parameters

# View all timeout parameters
sysctl -a | grep nf_conntrack_.*_timeout

# Key timeout parameter adjustments
# TCP established connection timeout (default 432000 seconds = 5 days, too long)
net.netfilter.nf_conntrack_tcp_timeout_established = 3600

# TIME-WAIT state timeout (default 120 seconds)
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30

# CLOSE_WAIT state timeout (default 60 seconds)
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 30

# SYN_SENT state timeout (default 120 seconds)
net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 30

The default tcp_timeout_established of 5 days is a major pitfall — a large number of idle connections will occupy table entries for extended periods. On load balancers, it should be set to 1 hour.

Monitoring conntrack Usage

#!/bin/bash
# conntrack monitoring script
CURRENT=$(cat /proc/sys/net/netfilter/nf_conntrack_count)
MAX=$(cat /proc/sys/net/netfilter/nf_conntrack_max)
RATIO=$(echo "scale=2; $CURRENT * 100 / $MAX" | bc)

echo "conntrack: $CURRENT / $MAX ($RATIO%)"

if (( $(echo "$RATIO > 80" | bc -l) )); then
    echo "WARNING: conntrack table usage above 80%!"
fi

NIC Interrupts and RPS/RFS/XPS Multi-Queue Binding

Modern multi-core servers’ NICs typically support multi-queue, where each queue corresponds to a hardware interrupt that can be bound to a different CPU core for parallel packet processing.

Viewing NIC Queues and Interrupts

# View NIC queue count
ethtool -l eth0

# Example output
# Channel parameters for eth0:
# Pre-set maximums:
# RX:		0
# TX:		0
# Other:		0
# Combined:	8       # Maximum 8 combined queues
# Current hardware settings:
# RX:		0
# TX:		0
# Other:		0
# Combined:	8       # Currently using 8 queues

# Set queue count (typically equal to CPU core count)
sudo ethtool -L eth0 combined 8

# View interrupt numbers
cat /proc/interrupts | grep eth0

# Example output
#            CPU0       CPU1       CPU2       CPU3
#  31:   ...   0          0          0          0   PCI-MSI  -eth0-rx-0
#  32:   ...   0          0          0          0   PCI-MSI  -eth0-rx-1
#  33:   ...   0          0          0          0   PCI-MSI  -eth0-tx-0
#  34:   ...   0          0          0          0   PCI-MSI  -eth0-tx-1

Binding Interrupts to CPU Cores

#!/bin/bash
# Bind eth0's RX queue interrupts evenly across CPU cores
# Suitable for 8-core CPU + 8-queue NIC

IRQS=$(grep eth0-rx /proc/interrupts | awk -F: '{print $1}' | tr -d ' ')
CPU=0
MASKS=(1 2 4 8 10 20 40 80)  # CPU0-CPU7 masks (hexadecimal)

for irq in $IRQS; do
    # Set interrupt affinity
    printf "%x" $((1 << $CPU)) > /proc/irq/$irq/smp_affinity
    echo "IRQ $irq -> CPU$CPU"
    CPU=$((CPU + 1))
done

Note: smp_affinity takes a bitmask. For example, 00000001 means CPU0, 00000002 means CPU1, 00000003 means CPU0+CPU1.

RPS (Receive Packet Steering)

When the NIC does not support multi-queue or the queue count is less than the CPU core count, RPS achieves a similar effect in software — distributing receive softirqs across multiple CPU cores:

# Set eth0's rx-0 queue to be handled by CPU0-CPU7 (8-core mask: ff)
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus

# Set for all RX queues
for i in /sys/class/net/eth0/queues/rx-*/rps_cpus; do
    echo ff > $i
done

# Set RPS flow table size (improves flow distribution accuracy)
echo 32768 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

RFS (Receive Flow Steering)

RFS further optimizes on top of RPS: instead of just distributing packets across different CPUs, it sends packets from the same connection to the CPU where the application processing that connection resides, improving CPU cache hit rates:

# Global RFS flow table size (recommend = RPS queue count × per-queue rps_flow_cnt)
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

# Per-queue flow table size
for i in /sys/class/net/eth0/queues/rx-*/rps_flow_cnt; do
    echo 4096 > $i
done

XPS (Transmit Packet Steering)

XPS is a transmit-side optimization that specifies which CPUs can use which transmit queues, reducing lock contention on the transmit path:

# Set tx-0 queue to be used by CPU0 only
echo 01 > /sys/class/net/eth0/queues/tx-0/xps_cpus

# 8 queues mapped to 8 cores
QUEUES=(0 1 2 3 4 5 6 7)
CPUS=(01 02 04 08 10 20 40 80)
for i in "${!QUEUES[@]}"; do
    echo ${CPUS[$i]} > /sys/class/net/eth0/queues/tx-${QUEUES[$i]}/xps_cpus
done

RPS/RFS/XPS Comparison

TechnologyDirectionPurposeHardware Requirement
Interrupt bindingRXDistribute hardware interrupts across coresNIC with multi-queue support
RPSRXDistribute soft interrupts across coresNone (pure software)
RFSRXDistribute by connection affinity to the application’s coreNone (pure software)
XPSTXBind transmit queues to specific cores, reducing lock contentionNone (pure software)

TCP BBR Congestion Control

BBR (Bottleneck Bandwidth and RTT) is a congestion control algorithm developed by Google, merged into the Linux kernel in 2016 (4.9+). Unlike the traditional CUBIC algorithm, which infers congestion from packet loss, BBR controls the sending rate by probing the bottleneck bandwidth and round-trip time.

Enabling BBR

# View the current congestion control algorithm
sysctl net.ipv4.tcp_congestion_control
# Default output: net.ipv4.tcp_congestion_control = cubic

# View algorithms supported by the kernel
sysctl net.ipv4.tcp_available_congestion_control
# Output: net.ipv4.tcp_available_congestion_control = reno cubic bbr

# If bbr is not listed, load the module
sudo modprobe tcp_bbr

# Enable BBR
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr

# Also set the default qdisc to fq (BBR recommends pairing with fq queue scheduling)
sudo sysctl -w net.core.default_qdisc=fq

Making It Permanent

# Write to /etc/sysctl.d/99-bbr.conf
sudo tee /etc/sysctl.d/99-bbr.conf << 'EOF'
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
EOF

# Load and verify
sudo sysctl -p /etc/sysctl.d/99-bbr.conf
sysctl net.ipv4.tcp_congestion_control
# Output: net.ipv4.tcp_congestion_control = bbr

BBR vs CUBIC Performance Comparison

BBR’s core advantage is most pronounced in long-fat pipe (high bandwidth × high latency) scenarios:

# Benchmark using iperf3
# Server side
iperf3 -s

# Client side - CUBIC
sudo sysctl -w net.ipv4.tcp_congestion_control=cubic
iperf3 -c <server_ip> -t 30 -P 4

# Client side - BBR
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
iperf3 -c <server_ip> -t 30 -P 4

Typical results comparison (cross-region scenario):

ScenarioCUBIC ThroughputBBR ThroughputImprovement
Same datacenter (RTT < 1ms)9.8 Gbps9.9 GbpsNegligible
Cross-city (RTT ~30ms)2.1 Gbps7.8 Gbps3.7x
Cross-continent (RTT ~200ms)180 Mbps1.2 Gbps6.7x
Lossy network (1% loss)45 Mbps890 Mbps19.8x

BBR’s improvement is particularly significant on lossy networks, because CUBIC drastically reduces speed on any packet loss, while BBR does not rely on loss signals.

BBR Considerations

  1. BBR v1 vs v2: Kernel 5.4+ supports BBR v2 (in the tcp_bbr2 module). v2 fixes v1’s fairness issues on shallow-buffer links and bufferbloat problems. If your kernel version supports it, v2 is recommended.
  2. Public cloud limitations: Some cloud providers (e.g., AWS) have kernels that do not support loading custom BBR modules; use the cloud provider’s optimized kernel.
  3. Not a silver bullet: BBR optimizes single-connection throughput. For scenarios with many short connections (e.g., HTTP APIs), connection establishment overhead and TLS handshake are the bottlenecks; the congestion control algorithm has little impact.

Production Tuning Checklist

Below is a complete sysctl.conf configuration for high-concurrency web servers, validated in production:

# /etc/sysctl.d/99-network-tuning.conf

##############################################
# Connection Queues
##############################################
# Accept queue
net.core.somaxconn = 65535
# SYN queue
net.ipv4.tcp_max_syn_backlog = 65535
# NIC receive queue
net.core.netdev_max_backlog = 250000

##############################################
# TCP Parameters
##############################################
# Reuse TIME-WAIT ports (only effective for outbound connections)
net.ipv4.tcp_tw_reuse = 1
# TIME-WAIT count limit
net.ipv4.tcp_max_tw_buckets = 1048576
# SYN+ACK retry count (prevents SYN Flood)
net.ipv4.tcp_synack_retries = 2
# SYN retry count
net.ipv4.tcp_syn_retries = 2
# FIN-WAIT-2 timeout (seconds)
net.ipv4.tcp_fin_timeout = 15

# TCP keepalive
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3

# TCP buffers (min/default/max)
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.rmem_default = 1048576
net.core.wmem_default = 1048576

# Enable TCP Fast Open (saves one RTT)
net.ipv4.tcp_fastopen = 3

# Enable MTU probing
net.ipv4.tcp_mtu_probing = 1

# Disable slow start after idle (preserves window size for long connections)
net.ipv4.tcp_slow_start_after_idle = 0

# Local port range (available ports for outbound connections)
net.ipv4.ip_local_port_range = 10000 65535

##############################################
# conntrack (for load balancers / NAT gateways)
##############################################
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 30

##############################################
# Congestion Control
##############################################
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

##############################################
# Other
##############################################
# Enable reverse path filtering (prevents IP spoofing)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Disable ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
# Disable source routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
# Apply configuration
sudo sysctl -p /etc/sysctl.d/99-network-tuning.conf

# Verify key parameters
sysctl net.core.somaxconn net.ipv4.tcp_tw_reuse \
       net.ipv4.tcp_congestion_control net.netfilter.nf_conntrack_max

Tuning Verification Checklist

Check ItemCommandExpected
Accept queue overflownetstat -s | grep overflowedIncrement is 0
SYN queue overflownetstat -s | grep "SYNs to LISTEN"Increment is 0
conntrack full dropsdmesg | grep "table full"No output
Excessive TIME-WAITss -s | grep timewait< conntrack_max
BBR enabledsysctl tcp_congestion_controlbbr
Port exhaustionss -s | grep TCPestab < port_range

Summary

Linux network stack tuning is a systems engineering effort that spans multiple layers from the kernel protocol stack to NIC hardware. The core principles are:

  1. Monitor before tuning: Don’t blindly change parameters. First use netstat -s, ss -s, nstat, and conntrack monitoring to identify the bottleneck.
  2. Tune layer by layer: Connection queues → conntrack → NIC multi-queue → congestion control, tuning from highest to lowest impact.
  3. Understand principles, not memorize parameters: Knowing what each parameter controls enables correct trade-offs across different scenarios.
  4. Test and validate: Change only one group of parameters at a time, and validate with iperf3, wrk, or real traffic.

Further reading: Linux Network Stack Performance Optimization Guide (kernel.org) covers lower-level parameter definitions and principles, suitable for readers who need to deeply understand the kernel networking subsystem.

References & Acknowledgments

This article referenced the following materials during writing. We thank the original authors for their contributions:

  1. kernel.org documentation — Linux Kernel Organization, referenced for kernel.org documentation