DEV Community

Cover image for Measure bufferbloat on your internet connection with a single Python file
pretjeuh
pretjeuh

Posted on

Measure bufferbloat on your internet connection with a single Python file

Your video call sounds fine — until someone starts a big download. Suddenly you sound like you're calling from underwater. Lag spikes hit in games. SSH sessions stutter. Your bandwidth test shows 500 Mbps but nothing feels fast.

That's bufferbloat. And most people have never heard of it.


Contents


What is bufferbloat?

Every router and modem has packet queues — buffers that hold packets while they wait to be transmitted onto the line. When your link is fully saturated, those queues fill up. New packets arriving have to wait behind everything already queued. That wait is latency.

On a modern 1 Gbps fibre connection, this rarely matters — the line clears fast enough that queues stay short. But on the bottleneck link — typically your home router's uplink to the ISP — the math changes. A 50 Mbps upload link with a 1 MB buffer can hold 160ms of packets. When it fills up, every subsequent packet waits that long before even starting its journey.

The insidious part: your bandwidth looks fine. You're transmitting at full speed. It's just that every packet is sitting in a queue for hundreds of milliseconds first.

The term "bufferbloat" was coined by Jim Gettys around 2011 after he noticed his home DSL connection had catastrophically bad latency under load despite good throughput. He traced it to enormous buffers in consumer networking equipment — added by manufacturers because "bigger buffers = better performance" seemed intuitive, even though the opposite is often true.

Why large buffers hurt

TCP congestion control works by detecting packet loss: if a packet doesn't arrive, the sender backs off. This mechanism assumes that packets get dropped when a queue fills up. But if the queue is enormous, packets don't get dropped — they just wait. The sender never sees loss, so it never backs off. The queue stays permanently full.

Result: full throughput, catastrophic latency. Every interactive packet — ACKs, DNS queries, game state updates, video call audio — sits behind a mountain of bulk transfer data.

The fix: Active Queue Management

Active Queue Management (AQM) algorithms deliberately drop or mark packets before queues fill up, using that signal to tell senders to back off earlier. The two most effective modern algorithms:

  • fq_codel (Flow Queuing Controlled Delay) — separates traffic into per-flow queues, applies CoDel's controlled delay target to each. Ships in Linux since 3.5, available on OpenWrt and most Linux-based routers.
  • CAKE (Common Applications Kept Enhanced) — more sophisticated successor to fq_codel. Handles overhead compensation for PPPoE/VLAN encapsulation, has built-in traffic shaping, and performs better at low bandwidths. The current state of the art for home routers.

The problem: most ISP-supplied modems and consumer routers don't enable either. Some don't support them at all. Before you can fix it, you need to know how bad it is.


Why your ISP's speed test won't show it

Speedtest.net and Fast.com measure throughput. They don't measure latency under load. A connection with 500 Mbps download and 400ms of bufferbloat looks identical to a connection with 500 Mbps download and 5ms of bufferbloat on a standard speed test.

The Waveform Bufferbloat Test and similar tools do test this — but they're web-based, opaque, and you can't run them locally or automate them. That's the gap this tool fills.


What the tool does

bufferbloat.py runs a multi-phase load test while measuring round-trip time (RTT) at 1 Hz throughout. It compares idle latency against loaded latency and grades your connection A–F.

The five phases are:

  1. Baseline — measure idle RTT with no load, establishing the true unloaded latency
  2. Download — saturate the download path with 4 parallel HTTP threads
  3. Upload — saturate the upload path with 4 parallel HTTP POST threads
  4. Bidirectional — both directions simultaneously, the worst case
  5. Recovery — measure how fast latency returns to baseline after load stops

"Bloat" is calculated as the difference between loaded median RTT and baseline median RTT. Grades are based on the worst bloat seen across any loaded phase:

Grade Added latency under load Experience
A < 5 ms Excellent — barely noticeable
B < 30 ms Good — minor impact on sensitive apps
C < 60 ms Moderate — video calls degrade under load
D < 200 ms Poor — noticeable lag spikes, games unplayable
F > 200 ms Severe — SSH stutters, calls drop

My own results: a D on upload

I ran this on my own connection — ISP-supplied modem-router combo, fibre to the cabinet, QoS settings untouched since installation.

Baseline: 8ms RTT to Amsterdam

Download phase: 11ms median — 3ms bloat, essentially nothing

Upload phase: 112ms median — 104ms bloat, grade D

Bidirectional: 89ms median — 81ms bloat, grade D

Download was fine. Upload was the problem. The uplink buffer was filling up completely under load and staying full, adding over 100ms to every packet. Meanwhile my speed test showed a healthy 40 Mbps upload. Nothing in the ISP dashboard flagged anything unusual.

The asymmetry makes sense in hindsight: my download is ~200 Mbps, my upload is 40 Mbps. The upload bottleneck is tighter and the modem's uplink queue is apparently enormous.

This is the common pattern. Download buffers are less likely to cause problems because TCP ACKs travel upstream — when the downstream is saturated, ACK traffic competes for the (smaller, often less congested) upstream, partially self-limiting. Upload saturation has no such natural relief valve.


Running it — CLI mode

It's a single Python file. On first run it creates its own virtual environment and installs the two dependencies (matplotlib and flask) automatically.

git clone https://github.com/pretjeuh/bufferbloat
cd bufferbloat
python3 bufferbloat.py --target amsterdam
Enter fullscreen mode Exit fullscreen mode

There are 30+ geographic targets — CDN77 Points of Presence (PoPs) and major DNS providers across Europe, North America, and Asia-Pacific. The tool probes the target before starting and warns you if it's unreachable.

# See all available targets
python3 bufferbloat.py --list-targets

# Use your router as the ping target (measures internal queue separately)
python3 bufferbloat.py --ping-host 192.168.1.1

# Longer phases, skip upload (for restricted networks)
python3 bufferbloat.py --target frankfurt --duration 20 --no-upload

# Verbose: print each RTT measurement as it arrives
python3 bufferbloat.py --target amsterdam --verbose
Enter fullscreen mode Exit fullscreen mode

Tip on ping target choice: pinging your router (--ping-host 192.168.1.1) measures only the queue inside your router, not the ISP's equipment. Pinging an external host measures the total path including ISP buffers — usually what you want. If your router ping is clean but external ping shows bloat, the problem is upstream at the ISP's DSLAM or CMTS.


Running it — Web GUI mode

python3 bufferbloat.py --gui
Enter fullscreen mode Exit fullscreen mode

Opens a local Flask web app. Pick a target, watch the RTT chart update live as each phase runs.

Web GUI demo showing live RTT chart

When the test finishes, you can download the full self-contained HTML report — useful for comparing runs before and after making changes.


How it works under the hood

Load generation

Load uses plain Python — urllib.request for downloads, HTTP POST for uploads, with 4 parallel threads per direction hitting Cloudflare's speed test endpoints (anycast, so they hit your nearest PoP). No iperf3, no custom servers, nothing to install beyond the script itself.

Four threads is usually enough to saturate any home connection. If you have a multi-gigabit symmetric fibre line, you might need more — but for typical DSL, cable, and VDSL connections where bufferbloat is most common, 4 threads saturates the bottleneck.

Latency measurement

Ping runs once per second via the system ping binary — cross-platform on Windows, macOS, and Linux. A regex extracts the RTT value; timeouts (no reply within 2s) count as packet loss and are recorded separately.

One ping per second is deliberately conservative. More frequent pings would give a smoother RTT curve but also inject more ICMP traffic that might get deprioritised differently from regular traffic. 1 Hz gives enough data points per phase without interfering with the load test itself.

The GUI stream

The GUI uses Server-Sent Events (SSE) from a Flask /stream endpoint. Each rtt, phase_start, and phase_complete event updates a canvas chart drawn in plain JavaScript — no Chart.js, no external dependencies. A keepalive comment every 15 seconds prevents the SSE connection from timing out during quiet stretches.

Before starting, the /probe endpoint does a quick single ping to the selected target. If it doesn't respond, you get a warning before the test wastes your time.


The report and what to do about your grade

The HTML report is fully self-contained — inline CSS, base64-encoded PNG charts, works offline and is easy to share.

It includes:

  • RTT timeline with phase boundaries marked
  • Box plot of RTT distribution per phase (shows spread, not just median)
  • Phase table: min / median / P90 / max / packet loss / throughput / bloat / grade
  • Overall grade
  • Grade-specific remediation advice

The remediation section is the part I built this for. Getting a grade is useful; knowing what to actually do about it is more useful.


What actually fixes bufferbloat

If you have an OpenWrt router

This is the best case. OpenWrt ships CAKE and fq_codel. Enable SQM (Smart Queue Management):

opkg update && opkg install luci-app-sqm
Enter fullscreen mode Exit fullscreen mode

Then in LuCI: Network → SQM QoS → Enable, set your interface (usually eth1 or pppoe-wan), set download and upload speeds to ~95% of your measured line rate, and pick CAKE with piece_of_cake.qos as the queue discipline.

The 95% headroom is important — CAKE needs to be the bottleneck, not your ISP's equipment. If you set it to 100% or higher, the ISP's buffer fills before CAKE can act.

If you have DD-WRT

Navigate to NAT/QoS → QoS and enable it. Set the upload speed to ~90% of measured. DD-WRT's QoS is less sophisticated than CAKE but still better than nothing.

If you have OPNsense or pfSense

OPNsense: Interfaces → [WAN interface] → Edit → check "Enable Traffic Shaping" and configure HFSC or CAKE via the traffic shaper.

pfSense: Firewall → Traffic Shaper → HFSC. Alternatively, install the pfSense-pkg-cake package if available for your version.

If you have a consumer router with no AQM support

Your options:

  1. Enable bridge mode on your ISP modem and put a proper router (OpenWrt-capable hardware like a GL.iNet or Belkin RT3200) behind it
  2. Flash OpenWrt on your existing router if it's supported — check openwrt.org/toh
  3. Use a Raspberry Pi as a router — runs Linux natively, full CAKE support
  4. Contact your ISP — some ISPs will enable AQM on their equipment if asked, especially if you can show them test data

If you're on Wi-Fi

Run the test wired first. Wi-Fi adds its own variable latency on top of bufferbloat, and it's hard to separate the two sources of latency spikes. If your wired result is A/B but Wi-Fi is C/D, the problem is Wi-Fi contention, not bufferbloat — and the fix is different (less congested channel, Wi-Fi 6, mesh placement).


The deeper problem: AQM isn't a complete solution

Worth knowing: AQM fixes the queue problem but not all latency spikes under load.

A 2022 APNIC research paper (Beyond Bufferbloat) makes the argument that end-to-end congestion control has a fundamental limitation: when link capacity drops suddenly (common on Wi-Fi and 5G), packets queue up before the sender receives any congestion signal. Even with perfect AQM, there's an unavoidable minimum latency spike equal to roughly one RTT's worth of in-flight data.

For typical home broadband over wired ethernet or stable DSL, this matters less — capacity is relatively stable. For mobile and Wi-Fi connections where capacity can halve in milliseconds due to channel conditions, it's a real constraint.

The practical implication: if you test over Wi-Fi and still see latency spikes after enabling CAKE, you might be hitting this fundamental limit rather than a queue management problem. Test wired to isolate it.


Try it yourself

git clone https://github.com/pretjeuh/bufferbloat
cd bufferbloat
python3 bufferbloat.py --target amsterdam
Enter fullscreen mode Exit fullscreen mode

The project is open source — github.com/pretjeuh/bufferbloat. Issues and PRs welcome — especially if you find a target that doesn't respond reliably, or a platform where ping parsing breaks.

What did you score? Drop your grade, your router model, and whether you're on DSL/cable/fibre in the comments. Curious how widespread the D/F club is — and whether the upload-is-worse pattern holds up across more connections.

Top comments (0)