Modern Linux servers support a variety of TCP congestion control algorithms—ranging from loss-based models like Reno and Cubic to model-based rate controllers like BBR (Bottleneck Bandwidth and RTT). While high-bandwidth WAN connections benefit significantly from BBR’s immunity to shallow buffer packet loss, low-latency intra-datacenter RPCs often perform better with Cubic or DCTCP. Applications can dynamically select congestion algorithms per connection using the TCP_CONGESTION socket option. Tracing these switches using extended Berkeley Packet Filter (eBPF) kprobes illuminates real-time congestion state adaptations. This tutorial demonstrates how to trace algorithm changes on Linux.
The Mechanics of TCP Congestion Algorithm Switching
When an application configures setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "bbr", 3), the Linux kernel invokes tcp_set_congestion_control(). The kernel releases the previous congestion control ops structure (icsk->icsk_ca_ops), initializes the new algorithm’s private state via its init() callback, and resets congestion window (snd_cwnd) calculations. By attaching eBPF probes to this transition point, system operators can detect runtime misconfigurations and measure performance shifts.
Prerequisites and Environment Verification
Install the required tracing tools on your Linux server:
sudo apt-get update
sudo apt-get install -y bpftrace linux-headers-$(uname -r)
Verify available kernel congestion modules:
sysctl net.ipv4.tcp_available_congestion_control
If BBR is not listed, load it via sudo modprobe tcp_bbr.
Authoring the bpftrace TCP_CONGESTION Monitor
Save the following script as trace_congestion.bt to monitor runtime congestion control algorithm substitutions:
#!/usr/bin/env bpftrace
#include <net/sock.h>
#include <net/tcp.h>
kprobe:tcp_set_congestion_control
{
$sk = (struct sock *)arg0;
$name = str(arg1);
$inet = (struct inet_sock *)arg0;
$daddr = ntop($inet->sk.__sk_common.skc_daddr);
$dport = bswap($inet->sk.__sk_common.skc_dport);
$saddr = ntop($inet->sk.__sk_common.skc_rcv_saddr);
$sport = bswap($inet->inet_sport);
printf("[CA SWITCH] %s (PID %d) changed socket %s:%d -> %s:%d to algorithm '%s'\n",
comm, pid, $saddr, $sport, $daddr, $dport, $name);
@ca_switches[$name] = count();
@switches_by_app[comm] = count();
}
kprobe:tcp_ca_openreq_child
{
$name = str(arg1);
@listener_ca_inherited[$name] = count();
}
END
{
printf("\n--- Congestion Algorithm Selection Summary ---\n");
print(@ca_switches);
print(@switches_by_app);
print(@listener_ca_inherited);
}
Running the Trace and Observing Runtime Switches
Execute the bpftrace script with root privileges:
sudo bpftrace trace_congestion.bt
Test with curl or an application configured to request BBR: curl --tcp-fastopen https://example.co.uk. The trace output records the transition immediately:
[CA SWITCH] envoy (PID 14205) changed socket 10.0.1.20:49210 -> 104.244.42.1:443 to algorithm 'bbr'
[CA SWITCH] nginx (PID 8821) changed socket 10.0.1.20:49212 -> 151.101.1.69:443 to algorithm 'cubic'
Tuning Congestion Control Globally and Locally
- To set BBR as the global default for all newly initiated connections across the system, configure
/etc/sysctl.d/99-bbr.conf:net.core.default_qdisc = fq net.ipv4.tcp_congestion_control = bbr - Apply immediately with
sudo sysctl --system. - Ensure Fair Queueing (
fq) is selected as the default queueing discipline, as BBR relies on packet pacing provided byfq.