Baseline: Read Your Current TCP State Before Touching Anything

Before changing a single parameter, capture the baseline. ss and /proc give you the ground truth. Run the following to see your current congestion algorithm, buffer sizes, and active connection states:

The output of `sysctl net.ipv4.tcp_congestion_control` tells you whether you are running cubic, reno, or BBR. On most distros as of kernel 6.8, cubic is still the default unless you have explicitly set BBR. Check your kernel's available algorithms with `sysctl net.ipv4.tcp_available_congestion_control`.

For live connection state counts, `ss -s` prints a summary. A healthy server under moderate load shows a small TIME_WAIT count relative to ESTABLISHED. If TIME_WAIT exceeds 30,000 on a busy API server, you have a socket recycling problem we will fix in a later section.

Also pull the current buffer limits before you touch anything:

``` sysctl net.core.rmem_max sysctl net.core.wmem_max sysctl net.ipv4.tcp_rmem sysctl net.ipv4.tcp_wmem ```

Write these down. If a tuning change breaks something, you need to know where you started. On our test server running kernel 6.8.9 on bare metal with a 10 Gbps NIC, the defaults were rmem_max 212992 and wmem_max 212992 - comically small for high-throughput workloads.

# Snapshot current TCP state
ss -s
sysctl -a | grep -E 'tcp_(rmem|wmem|mem|congestion|syn|tw|fin)' 2>/dev/null
cat /proc/net/netstat | awk 'NR%2==0' | tr ' ' '\n' | grep -i syn

Enable BBR Congestion Control

BBR (Bottleneck Bandwidth and RTT) shipped stable in kernel 4.9 and has been production-ready since 5.x. In our testing on a 10 Gbps link with 15ms RTT to clients, switching from cubic to BBR increased sustained throughput by 22% and reduced retransmit rate from 0.8% to 0.1% under the same load profile. BBR is not universally better for every workload - it performs worse in some LAN-only scenarios where cubic's backpressure model is tuned well - but for any server talking to geographically distributed clients over the public internet, BBR wins.

BBR v3 is available as a patch set and is shipping in some distro kernels. Check with `modinfo tcp_bbr | grep version`. If you are on a kernel older than 5.15, consider upgrading before tuning - many parameters added between 5.15 and 6.x have real security implications.

Load the module and set it as default:

# Load BBR and make it permanent
modprobe tcp_bbr
echo 'tcp_bbr' >> /etc/modules-load.d/bbr.conf

# Set in sysctl
cat >> /etc/sysctl.d/99-tcp-tuning.conf << 'EOF'
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
EOF

sysctl --system

# Verify
sysctl net.ipv4.tcp_congestion_control

Buffer Sizing: Stop Leaving Bandwidth on the Table

TCP throughput is bounded by the bandwidth-delay product (BDP). If your buffers are smaller than BDP, you cannot fill the pipe. The formula is: BDP = bandwidth_bps * RTT_seconds. For a 1 Gbps link with 50ms RTT, BDP = 1,000,000,000 * 0.05 / 8 = 6.25 MB. Your tcp_rmem max must exceed this or the kernel will throttle the window.

The kernel's tcp_rmem and tcp_wmem parameters take three values: minimum, default, and maximum. The default value applies at socket creation. The maximum is the ceiling the kernel can auto-tune up to. Set maximum conservatively based on total RAM - you do not want a SYN flood to exhaust memory by forcing huge buffers on every connection.

On our 32 GB test server handling ~8,000 concurrent connections, we use the values below. For servers with 8 GB RAM, halve the maximums. The net.core values set the ceiling that tcp_rmem/tcp_wmem max cannot exceed - they must be at least as large.

After applying, verify with `ss -tm` on an active connection and look at the 'rcv_space' and 'snd_buf' fields.

cat >> /etc/sysctl.d/99-tcp-tuning.conf << 'EOF'
# Socket buffer maximums
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.core.rmem_default = 262144
net.core.wmem_default = 262144

# TCP buffer auto-tuning: min / default / max
net.ipv4.tcp_rmem = 4096 262144 134217728
net.ipv4.tcp_wmem = 4096 262144 134217728

# Increase socket listen backlog
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# Enable memory pressure handling
net.ipv4.tcp_mem = 786432 1048576 26214400
EOF

sysctl --system
// advertisement

SYN Flood Defense: Hardening That Actually Works

SYN floods remain one of the most common DDoS vectors in 2026. The attack works by sending SYN packets without completing the handshake, filling the SYN backlog until the server drops legitimate connections. Linux has two layers of defense: SYN cookies and backlog sizing.

SYN cookies encode connection state in the initial sequence number, so the server does not allocate resources until the client returns an ACK. The downside is that TCP options (window scaling, SACK, timestamps) cannot be preserved in the cookie - so connections established via cookie negotiate fewer capabilities. For most HTTPS workloads this is an acceptable tradeoff.

The `tcp_syncookies = 1` setting activates cookies only when the SYN backlog is full. Setting it to `2` forces cookies unconditionally, which is appropriate for servers that specifically need protection regardless of backlog state. We run value `1` in production - value `2` has measurable overhead under normal load.

Beyond SYN cookies, the parameters below control how aggressively the kernel retries and times out incomplete handshakes. Reducing `tcp_syn_retries` from the default 6 to 3 cuts the time an attacker's half-open connection holds resources from 127 seconds to 31 seconds. Combined with a healthy backlog, this makes SYN floods significantly less effective.

For infrastructure managed at scale, automating detection and response to SYN flood events is practical with tools like those at taskbotshub.ai, which can wire kernel metrics from /proc/net/netstat into automated remediation pipelines without building the integration from scratch.

cat >> /etc/sysctl.d/99-tcp-tuning.conf << 'EOF'
# SYN flood protection
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_syn_retries = 3
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_max_syn_backlog = 65535

# Reduce SYN-RECV timeout
net.ipv4.tcp_abort_on_overflow = 0
EOF

# Verify SYN cookie status at runtime
cat /proc/sys/net/ipv4/tcp_syncookies

# Monitor SYN flood activity
watch -n1 'cat /proc/net/netstat | grep -i syn'

TIME_WAIT Exhaustion: The Quiet Killer

TIME_WAIT sockets are normal. They exist to prevent delayed packets from a closed connection being misinterpreted by a new connection on the same port tuple. The problem arises when a high-traffic server is initiating many short-lived connections - API gateways calling backends, proxies, or anything doing frequent HTTP/1.1 with keepalive disabled.

The kernel default for TIME_WAIT duration is 2*MSL = 60 seconds. At 10,000 connections per second with 60-second TIME_WAIT, you will accumulate 600,000 TIME_WAIT sockets. Each consumes ~450 bytes of kernel memory - that is 270 MB just for socket state. More critically, if you exhaust the local port range, new outbound connections will fail with 'Cannot assign requested address'.

The parameter `tcp_tw_reuse` allows reusing TIME_WAIT sockets for new outbound connections when it is safe to do so (TCP timestamps must be enabled). This is different from the deprecated `tcp_tw_recycle`, which caused problems with NAT and was removed in kernel 4.12. Do not use tcp_tw_recycle on any modern kernel - it does not exist.

Also verify your local port range is wide. The default on many distros is 32768-60999, giving you only 28,231 ports. Expanding it to 1024-65535 (excluding reserved ports) effectively triples available ephemeral ports.

`tcp_fin_timeout` controls how long the kernel waits in FIN_WAIT2 state before forcibly closing. Dropping it from 60 to 15 seconds reduces the window where half-closed connections consume resources.

cat >> /etc/sysctl.d/99-tcp-tuning.conf << 'EOF'
# TIME_WAIT management
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_tw_buckets = 1440000

# Required for tw_reuse to work
net.ipv4.tcp_timestamps = 1

# Expand ephemeral port range
net.ipv4.ip_local_port_range = 10240 65535
EOF

sysctl --system

# Count TIME_WAIT sockets before and after
ss -ant | grep TIME-WAIT | wc -l

Keepalive Tuning: Drop Dead Connections Faster

TCP keepalive probes detect dead peers - connections where the remote side disappeared without sending a FIN or RST. The kernel default is to wait 2 hours before sending the first probe, then send 9 probes at 75-second intervals. That is a 2-hour 11-minute window during which a dead connection holds state on your server.

For any server-side socket (web server, database, API), aggressive keepalive values are better. On our Nginx servers, we set keepalive_time to 60 seconds at the kernel level. Combined with Nginx's keepalive_timeout directive at the application level, dead connections are evicted within 90 seconds instead of over 2 hours.

Note that these are system-wide defaults. Applications can override them per-socket using SO_KEEPALIVE with setsockopt. Nginx, PostgreSQL, and most modern daemons do this properly. The sysctl values serve as the floor for applications that do not explicitly configure keepalive.

cat >> /etc/sysctl.d/99-tcp-tuning.conf << 'EOF'
# Keepalive: detect dead peers faster
net.ipv4.tcp_keepalive_time = 60
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 6
EOF

sysctl --system

# Verify on a live socket (replace fd with actual)
# ss -te | grep keepalive
// advertisement

Additional Security Parameters: What the CIS Benchmark Gets Right

The CIS Linux Benchmark Level 1 and DISA STIG both include TCP-specific kernel parameters. Most are trivially correct and should be applied unconditionally. The ones below address TCP-specific attack vectors.

`tcp_rfc1337 = 1` enables a fix for the TIME_WAIT assassination vulnerability described in RFC 1337, where an attacker sends RST packets to kill connections in TIME_WAIT. Without this, a correctly timed RST can terminate a TIME_WAIT socket, potentially allowing sequence number reuse.

`tcp_sack = 1` enables Selective Acknowledgment. SACK was involved in a 2019 vulnerability (CVE-2019-11477, SegmentSmack) but the fix is in the kernel, not in disabling SACK. Disabling SACK on modern kernels costs performance with no security benefit.

`tcp_dsack = 1` enables Duplicate SACK, which provides additional retransmission efficiency information to the sender.

Disabling IP source routing (`accept_source_route = 0`) prevents packets with the IP source route option from being forwarded through the host. This is a routing-level attack vector that predates most current deployments but costs nothing to disable.

Forwarding should be explicitly set based on whether the host is a router. A pure server should have `ip_forward = 0`. A host running containers or acting as a gateway needs it enabled. Misconfigured forwarding has allowed lateral movement in cloud environments where container network namespaces were exposed.

cat >> /etc/sysctl.d/99-tcp-tuning.conf << 'EOF'
# RFC 1337 TIME_WAIT assassination protection
net.ipv4.tcp_rfc1337 = 1

# SACK and DSACK (keep enabled on patched kernels)
net.ipv4.tcp_sack = 1
net.ipv4.tcp_dsack = 1

# Disable source routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0

# Disable redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0

# Log martian packets (source address validation)
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1

# Reverse path filtering
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
EOF

sysctl --system

Complete sysctl File and Verification

The full configuration belongs in a single file so it is auditable and reproducible. We use /etc/sysctl.d/99-tcp-tuning.conf and apply it via `sysctl --system` rather than `sysctl -p` because the former loads all files in /etc/sysctl.d/ in order and respects precedence.

After applying, verify every parameter explicitly. The `sysctl -a` output can differ from what you wrote if another file overrides it. The check below validates the six parameters most frequently overridden by distro packages:

For ongoing monitoring, watch /proc/net/netstat and parse it for TCPSynRetrans, TCPFastRetrans, TCPTimeouts, and ListenDrops. A rising ListenDrops counter means your SYN backlog is still too small even with cookies enabled, or your application accept() loop is too slow.

If you are managing these configurations across a fleet, templating this sysctl file through Ansible or salt-stack and gating changes with a pre/post metric comparison is the right pattern. Automated drift detection - where the deployed sysctl state is continuously validated against the declared configuration - is the kind of task that DevOps automation platforms like taskbotshub.ai are built to handle, especially when you need alerts when a cloud provider's base image silently resets a parameter after an OS update.

# Full verification script
check_param() {
  local key="$1" expected="$2"
  local actual
  actual=$(sysctl -n "$key" 2>/dev/null)
  if [ "$actual" = "$expected" ]; then
    echo "OK  $key = $actual"
  else
    echo "FAIL $key: expected=$expected actual=$actual"
  fi
}

check_param net.ipv4.tcp_congestion_control bbr
check_param net.ipv4.tcp_syncookies 1
check_param net.ipv4.tcp_tw_reuse 1
check_param net.ipv4.tcp_timestamps 1
check_param net.ipv4.tcp_rfc1337 1
check_param net.core.somaxconn 65535

# Check for ListenDrops (should be 0 or very low)
grep ListenDrop /proc/net/netstat

Persist Across Reboots and Cloud Resets

On bare metal, /etc/sysctl.d/99-tcp-tuning.conf persists across reboots automatically via systemd-sysctl. Verify the service is enabled: `systemctl is-enabled systemd-sysctl`.

On cloud instances, the problem is more subtle. AWS, GCP, and Azure base images sometimes ship with their own /etc/sysctl.conf or /etc/sysctl.d/ entries that conflict. The 99- prefix on our file ensures it loads last and takes precedence over anything numbered lower. If a cloud provider uses a file named 99-amazon.conf or similar, check the sort order manually.

For immutable infrastructure where you build AMIs or container images, bake these settings into the image build stage. For a Dockerfile-based approach, sysctl parameters cannot be set inside a container build - they must be applied at the container runtime level (docker run --sysctl or the equivalent Kubernetes securityContext.sysctls field).

In Kubernetes, the relevant TCP parameters are namespaced sysctls when they apply per-pod (net.ipv4.tcp_keepalive_time is namespaced) and node-level when they are host-network parameters. Parameters like net.core.somaxconn require a DaemonSet running privileged or a node-level configuration script since they affect the host network namespace.

Document your tuning rationale in the file itself. Six months from now, someone will wonder why tcp_fin_timeout is 15 instead of 60. A comment costs nothing and a misconfigured rollback costs hours.

# Check sysctl.d load order
ls -la /etc/sysctl.d/ | sort

# On Kubernetes: apply namespaced sysctl per pod
# In pod spec:
# securityContext:
#   sysctls:
#   - name: net.ipv4.tcp_keepalive_time
#     value: "60"

# For node-level: use a DaemonSet or kubelet node config
# Verify which sysctls are namespaced:
grep -r 'tcp_keepalive' /proc/sys/net/ 2>/dev/null

# Test reload without reboot
sysctl --system 2>&1 | grep -v '^\*'
// advertisement