⏱️ Reading time: 13 min

When you upload a large file and the speed drops by half as soon as someone else opens a video on the same network, it’s not magic: it’s TCP congestion control deciding, packet by packet, how much traffic the link can handle before losing data. Linux lets you change the algorithm that makes that decision with a single command, and the difference between CUBIC (the one you’re using today without knowing it) and BBR (the one Google designed in 2016) is measured in milliseconds of latency and real megabits per second.

📑 En este artículo
  1. TL;DR
  2. What Is TCP Congestion Control?
  3. Why Traffic Management Matters on Shared Networks
  4. How It Works: From Slow Start to Fast Recovery
  5. Practical Examples and How to Get Started
  6. Real-World Use Cases
  7. Common Mistakes and Best Practices
  8. Comparison With Alternatives
  9. Going Deeper: The BBR Model and What Comes Next
  10. Frequently Asked Questions
    1. What Is TCP Congestion Control, and Who Decides It, the Client or the Server?
    2. Why Do CDNs and Video Platforms Prefer BBR Over CUBIC?
    3. How Do I Know Which Congestion Algorithm My Connection Is Using Right Now?
    4. Does BBR Replace CUBIC in Every Case?
    5. How Does Bufferbloat Relate to These Congestion Algorithms?
  11. References

TL;DR

  • TCP congestion control adjusts the send window (cwnd) to make the most of the available bandwidth without saturating the network.
  • CUBIC has been the default algorithm in Linux since kernel 2.6.19 (2006) and grows the window using a cubic function of time.
  • BBR, published by Google in 2016, measures bandwidth and RTT instead of only reacting once a packet is lost.
  • sysctl net.ipv4.tcp_congestion_control shows and lets you change the active algorithm on any modern Linux system.
  • Choosing the wrong algorithm on a shared network can fill up the router’s buffer: this phenomenon is called bufferbloat.

What Is TCP Congestion Control?

TCP congestion control is the protocol mechanism that regulates how many segments a sender can have pending acknowledgment, adjusting that number based on the actual state of the network. It has been part of the standard since 1988 and prevents multiple connections from saturating a shared link until packets are lost in cascade.

Before 1988 it didn’t exist: TCP sent data as fast as the receiver would allow, without looking at the network in between. The result, documented by Van Jacobson in his 1988 paper, was a series of congestion collapses on the early NSFNET, with throughput between two nearby nodes crashing for hours. That incident drove the algorithm that today runs, in modified form, on every web server on the planet.

Why Traffic Management Matters on Shared Networks

The internet doesn’t reserve bandwidth per connection: thousands of TCP flows share the same cable, the same router, and the same packet queue. Without a mechanism to slow down sending when the path saturates, any large download would crowd out the other connections until they were left without service.

The cost of ignoring congestion isn’t just speed: it’s packet loss, retransmissions, and in the worst case a collapse where useful throughput falls much faster than demand grows. That’s why every modern operating system, from a home router to a Google server, implements some variant of this control.

How It Works: From Slow Start to Fast Recovery

The classic algorithm (Reno, and with some nuances, CUBIC) goes through several phases. In slow start, the congestion window (cwnd) starts at one segment and doubles every confirmed RTT, until it hits a threshold called ssthresh or until a packet is lost. This is deliberate exponential growth: better to quickly find out how much the network can handle than to start blindly with a fixed value.

Once it exceeds ssthresh, it enters congestion avoidance: growth becomes linear there, one additional segment per full RTT (additive increase). If a loss occurs, Reno cuts the window in half (multiplicative decrease); hence the name AIMD, which describes the entire family of loss-based algorithms.

In concrete terms, additive increase amounts to adding cwnd = cwnd + MSS*MSS/cwnd for every ACK received during congestion avoidance, an approximation that manages to add roughly one MSS per full RTT without needing to count RTTs explicitly. Multiplicative decrease is simply cwnd = cwnd / 2 at the first sign of loss. That asymmetry, climbing slowly and dropping fast, is intentional: it prioritizes draining already-detected congestion before testing the network’s limit again.

When loss is detected via three duplicate ACKs (not via timeout), the sender doesn’t go back to slow start: it enters fast recovery, retransmits only the lost segment, and resumes linear growth from a reduced window. A full timeout, on the other hand, does force a return to slow start from scratch, because it indicates the network has completely lost track of the connection.

stateDiagram-v2
 [*] --> SlowStart
 SlowStart --> CongestionAvoidance: cwnd reaches ssthresh
 CongestionAvoidance --> FastRecovery: loss via duplicate ACKs
 FastRecovery --> CongestionAvoidance: retransmitted segment confirmed
 SlowStart --> FastRecovery: loss via duplicate ACKs
 CongestionAvoidance --> SlowStart: retransmission timeout

CUBIC, the default algorithm on Linux, replaces congestion avoidance’s linear growth with a cubic function of the time elapsed since the last loss: it grows fast at first, flattens out near the point where it lost last time, and accelerates again if the network allows it. This gives it better bandwidth utilization on high-capacity, high-latency links (so-called long fat networks) than Reno’s pure AIMD.

CUBIC has been the default algorithm in the Linux kernel since version 2.6.19, released in 2006. Foto de lee daxin en Unsplash

Practical Examples and How to Get Started

All of this can be inspected and changed on any modern Linux distribution without installing anything extra. The first step is to see which algorithms are available in the kernel:

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

That output only lists the modules already loaded. To see which one new connections use by default:

$ sysctl net.ipv4.tcp_congestion_control
net.ipv4.tcp_congestion_control = cubic

BBR ships as a kernel module (tcp_bbr) in almost every distro since Linux 4.9, but it has to be loaded before you can select it:

$ sudo modprobe tcp_bbr
$ lsmod | grep bbr
tcp_bbr 20480 0

With the module loaded, changing the active algorithm takes a single line (the change doesn’t survive a reboot unless you add it to /etc/sysctl.conf):

$ sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
net.ipv4.tcp_congestion_control = bbr

To confirm which algorithm a specific active connection is using, ss -i shows it as part of each socket’s summary:

$ ss -i
...
 cubic wscale:7,7 rto:204 rtt:1.2/0.6 mss:1448 cwnd:10 ssthresh:7
 bbr wscale:7,7 rto:216 rtt:14.8/7.4 mss:1448 cwnd:28 pacing_rate 1.2Mbps

If the test runs within the same LAN with nothing to simulate, the difference between algorithms is barely noticeable: TCP congestion control matters when there’s real latency and loss. To reproduce those conditions on a test machine, tc with netem adds artificial latency and loss to an interface:

$ sudo tc qdisc add dev eth0 root netem delay 100ms loss 1%

With that rule active, running iperf3 between two machines (one as a server with iperf3 -s, the other as a client with iperf3 -c 192.168.1.10 -t 30) shows how CUBIC loses throughput with every loss it interprets as congestion, while BBR sustains throughput better because it doesn’t depend on that signal. To remove the simulation rule: sudo tc qdisc del dev eth0 root.

sequenceDiagram
 participant E as Sender
 participant R as Receiver
 E->>R: segment 1 with cwnd at 1
 R-->>E: ACK for segment 1
 Note over E,R: cwnd grows to 2
 E->>R: segment 2
 E->>R: segment 3
 R-->>E: ACK for segment 2
 R-->>E: ACK for segment 3
 Note over E,R: cwnd grows to 4

Real-World Use Cases

Google enabled BBR for YouTube traffic and its internal B4 network because CUBIC, by treating any loss as a congestion signal, performs poorly on long-distance links where part of the loss is radio noise or switching, not actual saturation. In a video call or a stream, that difference translates into less buffering and fewer quality drops.

Something similar happens on mobile and Wi-Fi networks: a burst of interference drops packets without any real congestion existing on the backbone network. CUBIC slows the window down just as if a router were saturated; BBR, which doesn’t base its decision on loss but on bandwidth and minimum RTT measurements, tends to sustain throughput better in that scenario.

Mobile operators face the same dilemma in the radio access network, where RTT varies second to second depending on how many users are connected to the same cell. An algorithm that confuses that variation with real congestion ends up slowing down connections that actually have bandwidth available, and that’s one of the reasons operators and CDNs are evaluating BBR or their own variants for the last mile of the connection.

At the other extreme, inside a data center with short, high-capacity links, many operators prefer variants like DCTCP (Data Center TCP), which uses ECN instead of loss to signal congestion and reacts with much finer adjustments, designed for microsecond-scale queues instead of paths with tens of milliseconds of RTT.

Common Mistakes and Best Practices

The most common mistake is assuming BBR is superior in every case. Several networking community studies have documented that when a BBR flow shares a saturated buffer with CUBIC flows, BBR can end up claiming a disproportionate share of the bandwidth because it doesn’t reduce its sending rate in response to the signal that does slow down CUBIC. On a mixed link, that’s not an improvement: it’s unfairness between connections.

Another common mistake is confusing the operating system’s congestion algorithm with a per-application setting. The sysctl value only defines the default for new sockets; any application can request a different algorithm with setsockopt(TCP_CONGESTION), and many networking frameworks already do this without the administrator even knowing.

Changing the sysctl value also doesn’t affect TCP connections that were already open at that moment: the algorithm is fixed when the connection is established and can’t be reassigned mid-session. If you need an already-running service to use the new algorithm, you have to restart that connection or the process that opened it.

⚠️ Heads up: if a home router or an ISP has oversized buffers, no congestion algorithm solves bufferbloat on its own: the full queue still accumulates milliseconds of latency before dropping a packet. The complementary solution there is active queue management (AQM) like CoDel or fq_codel, not switching from CUBIC to BBR.

Finally, loading the tcp_bbr module without verifying it actually took effect is a typical oversight: if modprobe fails silently (old kernel, module not compiled), sysctl -w returns an invalid argument error instead of applying the change, so check dmesg before assuming the algorithm changed.

Comparison With Alternatives

Choosing between Reno, CUBIC, BBR, and DCTCP depends on the link, not on which one is abstractly better:

AlgorithmWhen It’s UsedAdvantageLimitation
RenoAcademic reference, very old kernelsSimple to implement and reason aboutTakes several RTTs to recover speed after a loss
CUBICDefault on Linux, Android, and most web servers since 2006Good bandwidth utilization on high-capacity, high-latency linksInterprets any loss as congestion, even if it’s radio noise
BBRCDNs, video calls, mobile or satellite links with loss unrelated to congestionMeasures bandwidth and RTT instead of depending on packet lossCan take more bandwidth than CUBIC when they share the same buffer
DCTCPInternal data center networks with microsecond RTTReacts to ECN signals much finer than lossNeeds switches with ECN support and isn’t designed for the public internet

Going Deeper: The BBR Model and What Comes Next

Loss-based algorithms (Reno, CUBIC) assume a 1980s premise: if a packet is dropped, there’s congestion. BBR inverts that logic and builds an explicit model of the network with two variables, the maximum observed bandwidth (BtlBw) and the minimum observed RTT (RTprop), and calculates the ideal sending rate as the product of both, the rule known as BDP (bandwidth-delay product).

To keep that model up to date, BBR cycles through four phases: Startup (exponential growth similar to slow start), Drain (empties the excess data that Startup pushed into intermediate queues), ProbeBW (alternates brief increases and decreases in sending rate to keep measuring available bandwidth), and ProbeRTT (drastically reduces sending for an instant to re-measure the minimum RTT without interference from its own queues).

flowchart TD
 A["Startup: doubles sending every RTT"] --> B["Drain: empties the accumulated buffer"]
 B --> C["ProbeBW: alternates between raising and lowering sending"]
 C --> D["ProbeRTT: reduces sending to measure the minimum RTT"]
 D --> C

BBR also spreads out the sending of each window across the estimated RTT (pacing) instead of sending the entire window all at once, as Reno and CUBIC did. Sending in a burst momentarily saturates intermediate buffers even if the average is within what the network can handle; spacing out packets according to the measured bandwidth avoids that spike and is one of the reasons BBR tends to generate less actual loss in the routers it passes through.

An orthogonal complement to the congestion algorithm is ECN (Explicit Congestion Notification): instead of waiting for a router to drop a packet, it marks a bit in the IP header when the queue starts filling up, and the receiver echoes that mark back in the ACK. It’s an earlier and cheaper signal than loss, and it’s the basis for variants like DCTCP. Linux supports it, but it remains disabled by default on many distributions because some old middleboxes mishandle it.

BBR measures the bandwidth-delay product (BDP) instead of waiting to lose a packet. Foto de Mohammad Rahmani en Unsplash

Your next step: run sysctl net.ipv4.tcp_available_congestion_control on your own Linux machine and compare it with ss -i to see which algorithm each active connection is using right now.

📬 Get new articles by email

We only email about big articles (1-2 a month).

Frequently Asked Questions

What Is TCP Congestion Control, and Who Decides It, the Client or the Server?

Each end of the connection runs its own copy of the algorithm over its own send window. There’s no prior negotiation: each side independently decides, based on the ACKs it receives, how much it can send next.

Why Do CDNs and Video Platforms Prefer BBR Over CUBIC?

Because on mobile or long-distance links, a good part of packet loss doesn’t come from real congestion but from interference or switching. CUBIC slows down the same way regardless of the loss cause; BBR, by measuring bandwidth and RTT instead of depending on loss, sustains throughput better under those conditions.

How Do I Know Which Congestion Algorithm My Connection Is Using Right Now?

ss -i shows the active algorithm as part of the summary of each open TCP socket, along with the congestion window (cwnd) and the RTT measured at that moment.

Does BBR Replace CUBIC in Every Case?

No. In a buffer shared with CUBIC flows, BBR can take a disproportionate share of the bandwidth, and in data centers with microsecond RTT, DCTCP is usually preferred, since it uses ECN instead of a bandwidth and RTT model.

How Does Bufferbloat Relate to These Congestion Algorithms?

Bufferbloat happens when a router accumulates packets in an oversized queue before dropping any, which adds latency without any congestion algorithm noticing in time. The solution isn’t to change algorithms but to apply active queue management, like CoDel, on the router.

References

  • ACM Digital Library: Van Jacobson’s original paper, Congestion Avoidance and Control (1988).
  • IETF RFC 5681: the formal specification of TCP congestion control.
  • ACM Queue: BBR Congestion-Based Congestion Control, the Google paper that introduced BBR in 2016.
  • Linux Kernel Documentation: reference for network sysctl parameters, including tcp_congestion_control.
  • Wikipedia: a historical overview of TCP congestion control variants.

📱 Enjoying this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.

Featured image: Foto de Dimitri Karastelev en Unsplash

Did it work for you? Got a different error? Say so below: questions get answered and help the next reader.

Leave a comment
Categories: ProgrammingTutorials

Andrés Morales

Developer and AI researcher. Writes about language models, frameworks, developer tooling, and open source releases. Covers ML papers, the tech startup ecosystem, and programming trends.

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

You can include code inside <code>…</code> or, for several lines, <pre><code>…</code></pre>.

This site uses Akismet to reduce spam. Learn how your comment data is processed.