Congestion Control in the Wild: Why Wi-Fi Makes TCP Algorithms Behave Badly

TCP congestion control on Wi-Fi

Picture a bulk file transfer. Server's on gigabit fiber, nothing else on the wire, CPU bored stiff. You kick off the copy and watch the throughput graph climb, stumble, climb again, then fall flat on its face for three seconds at a time. Nothing changed on the wired side. The only new character in this story is the Wi-Fi hop between your laptop and the access point. If TCP were an animal, Wi-Fi would be a housecat: unpredictable, noisy, and constantly interrupting its meals.

What TCP thinks it's dealing with

TCP's congestion control was born in a world of wires. Loss means congestion, full stop. Round-trip time wobbles a little but mostly behaves. There's one bottleneck link and it queues bytes politely in order. Slow start ramps up cautiously, Reno and CUBIC back off hard the moment a packet goes missing, and the whole send rate gets its rhythm from returning ACKs, a mechanism folks call ACK clocking. Elegant, if your transport layer is copper or glass. But hand that same logic to a radio channel and the elegance evaporates fast, because none of those founding assumptions survive first contact with the air.

Where the radio breaks the fairy tale

Here's the long version of why your Wi-Fi link is lying to TCP about what's actually happening.

Start with the MAC layer's own stubbornness. 802.11 retries failed frames itself, often several times, before TCP ever hears about it. A dropped packet on paper becomes a latency spike in practice, and that spike is invisible to anyone watching for loss. TCP thinks the pipe is clean. It isn't! It's just slow and cagey about admitting it.

Then there's the bursting behavior baked into modern Wi-Fi. A-MPDU aggregation bundles dozens of frames into one transmission opportunity, fires them off in a cluster, then goes quiet while the radio waits its turn again. ACKs arrive bunched up rather than evenly spaced, which murders the gentle, metered rhythm that ACK-clocked senders expect. TCP wants a steady drip. Wi-Fi hands it a fire hose followed by a drought.

The access point itself is often the real culprit, not the air. One radio, dozens of clients, a single queue trying to serve them all. Bufferbloat creeps in as that queue fills during busy periods, and suddenly your ping times look like a seismograph during an earthquake - even though raw throughput numbers look fine on a speed test.

Rate adaptation algorithms like Minstrel add their own mischief. The moment the radio decides your link quality dropped, it steps down the PHY rate, sometimes drastically, and your available bandwidth falls off a cliff mid-transfer. TCP has no vocabulary for "the pipe just got physically narrower." It only knows how to react to loss and delay, so it misreads a capacity change as congestion and throttles accordingly, often overcorrecting.

Hidden nodes and collisions throw reordering into the mix too. Two stations that can't hear each other but can both hear the AP step on each other's transmissions, retries happen out of sequence, and packets land at the receiver in the wrong order. TCP's reordering heuristics, built for rare and mild shuffling, get triggered constantly.

And let's not forget the humble microwave oven, the neighbor's cordless phone, the fading caused by someone walking between you and the router. These interferers cause honest-to-goodness packet loss that has absolutely nothing to do with congestion, yet loss-based congestion control treats every dropped packet as a confession that the network is overloaded.

Stack ACK compression on top of all this, where clustered acknowledgments arrive in a rush, and you get a sender that alternates between frantic bursts and awkward silence. Add it all together: delay spikes, bursty delivery, and loss that has nothing to do with congestion, and you get spurious retransmits, wild congestion window swings, and recovery that takes far longer than it should.

What this looks like on your screen

The symptoms are recognizable once you know what to watch for. Slow start sometimes collapses entirely right after a rate downgrade or a retransmission storm, because the sender interprets the chaos as a congestion signal and slams the brakes. You'll see repeated fast retransmits, sometimes outright spurious timeouts, triggered purely by ACK clumping and reordering rather than genuine loss.

Run several short-lived flows against the same AP and watch total throughput nosedive thanks to head-of-line blocking inside the aggregation queue. Interactive apps, video calls, game traffic, will stutter even while your bandwidth test proudly reports triple digits. The telltale fingerprints: RTT variance that looks like a drunk man's handwriting, duplicate ACKs appearing in clusters, cwnd oscillating like a yo-yo, and retransmission bursts that correlate suspiciously with nothing on the wired side at all.

Fixes worth your actual time

Turn on Selective ACKs and DSACK if they aren't already, because they let a sender recover from reordering without panicking into a full retransmission cycle. Enable TCP timestamps for better RTT measurement under jittery conditions. Experiment with BBR against CUBIC, since BBR doesn't treat every loss event as gospel, though it has its own quirks when RTT itself is swinging wildly, so test rather than assume. Pacing at the sender smooths out the burst-then-silence pattern that aggregation creates.

Watch how Nagle's algorithm interacts with delayed ACKs on lossy links, it can introduce pauses you never intended. Size your socket buffers for wireless reality, not textbook wired defaults, and don't assume a stock RTO is forgiving enough for radio jitter. If you control both ends of the connection, QUIC over UDP gives you user-space command over pacing and loss recovery that kernel TCP simply won't hand you. On the application side, resist the urge to open five small TCP streams to the same AP when one well-managed stream will do, and build retries that are idempotent rather than leaning on TCP to paper over everything.

Knobs on the access point worth touching

Shrink the A-MPDU aggregation size if latency matters more than raw throughput. Turn on WMM for proper QoS tagging. Keep high-bandwidth and latency-sensitive traffic on separate SSIDs or even separate radios. Good firmware with sane rate adaptation and airtime fairness solves half your problems before you write a line of code. When it's an option, push anything latency-critical onto wired backhaul and let Wi-Fi carry what can tolerate a little roughness.

How to see it with your own eyes

Grab iperf3 for throughput, tcpdump or Wireshark with radiotap headers for the real radio-level picture, netem for manufacturing jitter and loss on demand, and hostapd's debug output for what the AP thinks is happening. Force a rate drop. Introduce interference deliberately. Toggle aggregation on and off and watch the difference. Run one long flow, then run six short ones, and compare. Log throughput, the instantaneous RTT distribution, duplicate ACK counts, retransmission timing, and the congestion window timeline using something like `ss` with ca_state visibility. The numbers won't lie to you, even when the radio does.

Wi-Fi is a noisy middleman that forces you to rethink TCP assumptions. Measure first, tune second, and when the stakes are high, give paced TCP or QUIC a proper test run before you trust the defaults.

← Back to the blog