How to Trace TCP Socket TCP_CONGESTION Algorithm Switches in Linux Using eBPF

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 by fq.

Get the best tech tips delivered straight to your inbox.

Join thousands of readers mastering Apple, Google, Microsoft, and Linux.