The Linux kernel exposes its network settings through sysctl, a runtime interface to the values under /proc/sys. The defaults on Ubuntu 24.04 are sensible for a general purpose machine, but a busy web server, a proxy or a host that moves large files over long distances can benefit from a handful of targeted changes.

In this tutorial you will measure a baseline, switch TCP congestion control to BBR, size the TCP buffers for your bandwidth and latency, raise the limits that matter for many concurrent connections, and make everything persistent in a single file under /etc/sysctl.d/. Each change is explained so you can decide whether it applies to your workload instead of pasting a long list of values.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS (kernel 6.8), for example a CubePath VPS. The settings also apply to Debian 12 and Rocky Linux 9.
  • A non-root user with sudo privileges.
  • Optionally, a second machine to run throughput tests against, with iperf3 installed.

Step 1 - Recording the current values and a baseline

Start by saving the current network settings so you can compare or roll back later:

sudo sysctl -a 2>/dev/null | grep -E '^net\.(core|ipv4)' > ~/sysctl-net-before.txt
wc -l ~/sysctl-net-before.txt

Look at the settings this guide changes:

sysctl net.ipv4.tcp_congestion_control net.core.default_qdisc net.core.somaxconn \
  net.ipv4.tcp_rmem net.ipv4.tcp_wmem net.core.rmem_max net.core.wmem_max

On a fresh Ubuntu 24.04 installation the output looks like this:

net.ipv4.tcp_congestion_control = cubic
net.core.default_qdisc = fq_codel
net.core.somaxconn = 4096
net.ipv4.tcp_rmem = 4096	131072	6291456
net.ipv4.tcp_wmem = 4096	16384	4194304
net.core.rmem_max = 212992
net.core.wmem_max = 212992

Install the tools used to measure and inspect traffic. iproute2 (for ss and nstat) is already present; iperf3 measures throughput:

sudo apt update
sudo apt install iperf3

If you have a second machine, run iperf3 -s on it and measure from this server for 30 seconds:

iperf3 -c remote_server_ip -t 30

Replace remote_server_ip with the address of that machine. Note the Bitrate and Retr (retransmissions) columns of the sender summary line; you will compare them at the end.

Step 2 - Enabling BBR congestion control

Congestion control decides how fast TCP sends data. The default algorithm, CUBIC, reduces its rate sharply whenever a packet is lost, which hurts throughput on long or slightly lossy paths. BBR estimates the available bandwidth and round-trip time instead, and usually achieves higher throughput with lower queueing delay. It is the most impactful single change for servers that send data to users over the internet.

Check that the kernel offers BBR:

sysctl net.ipv4.tcp_available_congestion_control
net.ipv4.tcp_available_congestion_control = reno cubic

BBR is built as the tcp_bbr module and is not listed until it is loaded. Load it now and on every boot:

sudo modprobe tcp_bbr
echo tcp_bbr | sudo tee /etc/modules-load.d/bbr.conf

Create the file that will hold all the settings from this guide:

sudo nano /etc/sysctl.d/99-network-tuning.conf

Add the first block:

# Congestion control: BBR with the fq packet scheduler
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

fq (Fair Queue) paces packets per flow, which is what BBR was designed around. Apply the file:

sudo sysctl -p /etc/sysctl.d/99-network-tuning.conf

The default_qdisc setting only applies to interfaces created after the change. Replace the scheduler on your existing interface now, using the interface name from ip route show default (eth0 in these examples):

sudo tc qdisc replace dev eth0 root fq

Verify both settings:

sysctl net.ipv4.tcp_congestion_control
tc qdisc show dev eth0
net.ipv4.tcp_congestion_control = bbr
qdisc fq 8001: root refcnt 2 limit 10000p flow_limit 100p buckets 1024 ...

New connections now use BBR. You can confirm it on a live connection with ss -ti, which shows bbr in the details of each TCP socket.

Step 3 - Sizing the TCP buffers

TCP can only have as much unacknowledged data in flight as its buffers allow. The amount needed is the bandwidth-delay product (BDP): bandwidth multiplied by round-trip time. For a 1 Gbit/s link and a client 100 ms away:

1,000,000,000 bit/s / 8 * 0.100 s = 12,500,000 bytes (about 12 MB)

The default maximum receive buffer (tcp_rmem, 6 MB) and send buffer (tcp_wmem, 4 MB) cap a single connection well below 1 Gbit/s at that distance. Raising the maximums to 16 MB covers most servers with up to 1 Gbit/s of bandwidth. The kernel still auto-tunes each connection starting from the default value, so the memory is only used by connections that actually need it.

Add this block to /etc/sysctl.d/99-network-tuning.conf:

# TCP buffers: min, default, max (bytes). Max sized for ~1 Gbit/s at ~100 ms RTT
net.ipv4.tcp_rmem = 4096 131072 16777216
net.ipv4.tcp_wmem = 4096 16384 16777216
# Upper limit for buffers that applications set explicitly with setsockopt()
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

Two related settings help long-lived connections:

# Do not fall back to a small window after a connection has been idle
net.ipv4.tcp_slow_start_after_idle = 0
# Probe for a smaller MTU when ICMP "fragmentation needed" messages are filtered
net.ipv4.tcp_mtu_probing = 1

Apply and verify:

sudo sysctl -p /etc/sysctl.d/99-network-tuning.conf
sysctl net.ipv4.tcp_rmem net.ipv4.tcp_wmem
net.ipv4.tcp_rmem = 4096	131072	16777216
net.ipv4.tcp_wmem = 4096	16384	16777216

Step 4 - Handling many concurrent connections

Servers that accept thousands of connections per second, such as reverse proxies and API gateways, hit a different set of limits.

Accept queue. net.core.somaxconn caps the queue of fully established connections waiting for the application to call accept(). Ubuntu 24.04 already uses 4096; raise it only if you see overflows (see Step 6). The application must also request a large backlog, for example listen 443 backlog=8192; in Nginx, because the effective value is the smaller of the two.

SYN queue. tcp_max_syn_backlog limits half-open connections during the handshake.

Local ports. Each outgoing connection from a proxy to its backends uses a local port. The default range, 32768 to 60999, gives about 28,000 ports per destination address and port. Widen it on proxies.

Add this block to /etc/sysctl.d/99-network-tuning.conf:

# Connection handling
net.core.somaxconn = 8192
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.ip_local_port_range = 10240 65535

If a service on the server listens on a port inside the new range, the kernel may hand that port to an outgoing connection first. Check your listening ports with sudo ss -ltn and keep the start of the range above them, or reserve them with net.ipv4.ip_local_reserved_ports.

Settings you do not need to add: net.ipv4.tcp_tw_reuse already defaults to 2 (reuse for loopback), net.ipv4.tcp_syncookies is already 1, and net.ipv4.tcp_tw_recycle no longer exists in current kernels. Copying it from old guides produces an error.

Apply the file and verify:

sudo sysctl -p /etc/sysctl.d/99-network-tuning.conf
sysctl net.core.somaxconn net.ipv4.ip_local_port_range
net.core.somaxconn = 8192
net.ipv4.ip_local_port_range = 10240	65535

Step 5 - Raising the connection tracking limit

If the server uses a stateful firewall (UFW, iptables or nftables with ct state rules) or Docker, the kernel tracks every connection in the conntrack table. When the table is full, new connections are dropped silently. Check whether the module is loaded and how full the table is:

cat /proc/sys/net/netfilter/nf_conntrack_count /proc/sys/net/netfilter/nf_conntrack_max
1834
262144

If the files do not exist, conntrack is not in use and you can skip this step. If the count regularly reaches a large fraction of the maximum, raise the limit. Each entry uses roughly 300 bytes of kernel memory, so 1,048,576 entries cost about 300 MB at full occupancy:

# Connection tracking (only if nf_conntrack is loaded)
net.netfilter.nf_conntrack_max = 1048576

Because this key only exists once the nf_conntrack module is loaded, load it at boot as well so the setting applies reliably:

echo nf_conntrack | sudo tee /etc/modules-load.d/conntrack.conf
sudo sysctl -p /etc/sysctl.d/99-network-tuning.conf

Step 6 - Making the settings persistent and verifying them

Files in /etc/sysctl.d/ are applied at boot by systemd-sysctl. The final file should look like this:

cat /etc/sysctl.d/99-network-tuning.conf
# Congestion control: BBR with the fq packet scheduler
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

# TCP buffers: min, default, max (bytes). Max sized for ~1 Gbit/s at ~100 ms RTT
net.ipv4.tcp_rmem = 4096 131072 16777216
net.ipv4.tcp_wmem = 4096 16384 16777216
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_mtu_probing = 1

# Connection handling
net.core.somaxconn = 8192
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.ip_local_port_range = 10240 65535

# Connection tracking (only if nf_conntrack is loaded)
net.netfilter.nf_conntrack_max = 1048576

Load all configuration files in the same order as at boot and watch for errors:

sudo sysctl --system

Any line reporting cannot stat /proc/sys/... points to a key that does not exist on your kernel. Reboot once to confirm that everything survives a restart, then check a couple of values:

sudo reboot
sysctl net.ipv4.tcp_congestion_control net.core.somaxconn

Repeat the iperf3 test from Step 1 and compare the bitrate and retransmissions.

Watching the counters that matter

These commands tell you whether a limit is actually being hit, which is the only good reason to raise it:

# Connections dropped because the accept queue was full
nstat -az TcpExtListenOverflows TcpExtListenDrops

# Retransmitted segments since boot (compare over time, not as an absolute value)
nstat -az TcpRetransSegs

# Summary of sockets by state
ss -s

For a listening socket, ss -ltn shows the current accept queue length in Recv-Q and the configured backlog in Send-Q. A Recv-Q close to Send-Q under load means the application is not accepting connections fast enough.

Troubleshooting

sysctl: cannot stat /proc/sys/net/netfilter/nf_conntrack_max: No such file or directory. The conntrack module is not loaded. Either load it as shown in Step 5 or remove the line if you do not use a stateful firewall.

dmesg shows nf_conntrack: table full, dropping packet. The conntrack table is full. Raise nf_conntrack_max as in Step 5 and find out why so many connections are tracked, for example a flood of short connections that a rate limit should stop.

Cannot assign requested address errors in a proxy. The server ran out of local ports towards a backend. Widen ip_local_port_range, enable keepalive connections to the backend, or add more backend addresses.

Throughput did not improve. The bottleneck is elsewhere: the network link, the remote side's receive window, disk speed, or the application itself. Check the remote host's buffers too, because TCP speed is limited by both ends.

To undo everything, delete the file, the module files and reboot:

sudo rm /etc/sysctl.d/99-network-tuning.conf /etc/modules-load.d/bbr.conf /etc/modules-load.d/conntrack.conf
sudo reboot

Conclusion

You have enabled BBR with the fq scheduler, sized TCP buffers from the bandwidth-delay product, raised the queues and port range used by busy servers and set a conntrack limit, all in one file that is applied at boot. More importantly, you now know which counters show whether each limit is being reached.

As next steps, collect nstat and ss metrics with node_exporter or a similar agent so you can see trends over time, raise the open file limit (LimitNOFILE) of services that handle many sockets, and load test your application before and after each change.