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:
dmesgshowingnf_conntrack: table full, dropping packetss -sshowing a large number ofTIME-WAITconnections- 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.
TIME-WAIT Related Parameters
# 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_affinitytakes a bitmask. For example,00000001means CPU0,00000002means CPU1,00000003means 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
| Technology | Direction | Purpose | Hardware Requirement |
|---|---|---|---|
| Interrupt binding | RX | Distribute hardware interrupts across cores | NIC with multi-queue support |
| RPS | RX | Distribute soft interrupts across cores | None (pure software) |
| RFS | RX | Distribute by connection affinity to the application’s core | None (pure software) |
| XPS | TX | Bind transmit queues to specific cores, reducing lock contention | None (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):
| Scenario | CUBIC Throughput | BBR Throughput | Improvement |
|---|---|---|---|
| Same datacenter (RTT < 1ms) | 9.8 Gbps | 9.9 Gbps | Negligible |
| Cross-city (RTT ~30ms) | 2.1 Gbps | 7.8 Gbps | 3.7x |
| Cross-continent (RTT ~200ms) | 180 Mbps | 1.2 Gbps | 6.7x |
| Lossy network (1% loss) | 45 Mbps | 890 Mbps | 19.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
- BBR v1 vs v2: Kernel 5.4+ supports BBR v2 (in the
tcp_bbr2module). v2 fixes v1’s fairness issues on shallow-buffer links and bufferbloat problems. If your kernel version supports it, v2 is recommended. - 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.
- 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 Item | Command | Expected |
|---|---|---|
| Accept queue overflow | netstat -s | grep overflowed | Increment is 0 |
| SYN queue overflow | netstat -s | grep "SYNs to LISTEN" | Increment is 0 |
| conntrack full drops | dmesg | grep "table full" | No output |
| Excessive TIME-WAIT | ss -s | grep timewait | < conntrack_max |
| BBR enabled | sysctl tcp_congestion_control | bbr |
| Port exhaustion | ss -s | grep TCP | estab < 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:
- Monitor before tuning: Don’t blindly change parameters. First use
netstat -s,ss -s,nstat, and conntrack monitoring to identify the bottleneck. - Tune layer by layer: Connection queues → conntrack → NIC multi-queue → congestion control, tuning from highest to lowest impact.
- Understand principles, not memorize parameters: Knowing what each parameter controls enables correct trade-offs across different scenarios.
- 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:
- kernel.org documentation — Linux Kernel Organization, referenced for kernel.org documentation