<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: pretjeuh</title>
    <description>The latest articles on DEV Community by pretjeuh (@pretjeuh).</description>
    <link>https://dev.to/pretjeuh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3970010%2F44313f9c-5690-459d-90d0-8ce199f01e51.png</url>
      <title>DEV Community: pretjeuh</title>
      <link>https://dev.to/pretjeuh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pretjeuh"/>
    <language>en</language>
    <item>
      <title>ping-multiple.sh — a live terminal dashboard for monitoring multiple hosts</title>
      <dc:creator>pretjeuh</dc:creator>
      <pubDate>Tue, 16 Jun 2026 13:51:40 +0000</pubDate>
      <link>https://dev.to/pretjeuh/ping-multiplesh-a-live-terminal-dashboard-for-monitoring-multiple-hosts-jak</link>
      <guid>https://dev.to/pretjeuh/ping-multiplesh-a-live-terminal-dashboard-for-monitoring-multiple-hosts-jak</guid>
      <description>&lt;p&gt;After a migration or a firewall swap, you need to verify that a dozen endpoints are reachable — on the right protocols. Is SSH still up? Did HTTP come back? Is that database port open? Firing &lt;code&gt;ping&lt;/code&gt; or &lt;code&gt;nc&lt;/code&gt; one host at a time is slow and you lose the overview the moment you move to the next check.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ping-multiple.sh&lt;/code&gt; is a single bash script that watches all of them at once, live, in your terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4mf73ga17jo7tzotvtyx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4mf73ga17jo7tzotvtyx.png" alt="Dashboard filling up after ~26 seconds" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each row shows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Status&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UP&lt;/code&gt; / &lt;code&gt;SLOW&lt;/code&gt; / &lt;code&gt;DOWN&lt;/code&gt; / &lt;code&gt;...&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTT&lt;/td&gt;
&lt;td&gt;Latest round-trip time (&lt;code&gt;TO&lt;/code&gt; = timeout)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;IP or hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Probe&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;[ICMP]&lt;/code&gt;, &lt;code&gt;[SSH]&lt;/code&gt;, &lt;code&gt;[HTTP]&lt;/code&gt;, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bar&lt;/td&gt;
&lt;td&gt;60 most-recent results, newest on the right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sparkline&lt;/td&gt;
&lt;td&gt;RTT trend &lt;code&gt;▁▂▃▄▅▆▇█&lt;/code&gt; (ICMP only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Green = up/fast. Yellow = slow (above your threshold). Red = down. Gray = waiting for first sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ICMP and TCP probes per target&lt;/strong&gt; — mix and match on the same dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual probe cadence for ICMP&lt;/strong&gt;: a fast 1-second probe drives the bar, a slower 5-second probe drives the RTT column&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TCP connect latency&lt;/strong&gt; for SSH, HTTP, HTTPS, RDP, or any port&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State-change alerts&lt;/strong&gt; — terminal bell + optional &lt;code&gt;--log FILE&lt;/code&gt; for every UP↔DOWN transition&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTT sparkline&lt;/strong&gt; per host using 8-level block characters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSV export on exit&lt;/strong&gt; — per-host samples, loss %, min/avg/max RTT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive wizard&lt;/strong&gt; (&lt;code&gt;--guide&lt;/code&gt;) or &lt;strong&gt;CSV import&lt;/strong&gt; (&lt;code&gt;--csv targets.csv&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ICMP ping to two hosts&lt;/span&gt;
./ping-multiple.sh 8.8.8.8,1.1.1.1

&lt;span class="c"&gt;# Interactive setup — choose probe type per host&lt;/span&gt;
./ping-multiple.sh &lt;span class="nt"&gt;--guide&lt;/span&gt;

&lt;span class="c"&gt;# Load from a CSV file, log state changes&lt;/span&gt;
./ping-multiple.sh &lt;span class="nt"&gt;--csv&lt;/span&gt; targets.csv &lt;span class="nt"&gt;--log&lt;/span&gt; events.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CSV format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ip,label,probe
8.8.8.8,Google DNS,icmp
10.0.0.1,Web server,tcp:80
10.0.0.2,Database,tcp:5432
mail.example.com,Mail server,tcp:25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Every host gets its own set of background subshells. For ICMP targets there are two workers: a fast one (1s interval, 1s deadline) writing &lt;code&gt;G&lt;/code&gt;/&lt;code&gt;Y&lt;/code&gt;/&lt;code&gt;R&lt;/code&gt; chars to a ring-buffer file, and a slow one (5s interval, 5s deadline) updating the RTT column. TCP targets get a single worker that connects with &lt;code&gt;nc&lt;/code&gt; every 10 seconds and records connect latency.&lt;/p&gt;

&lt;p&gt;The display loop reads all the ring buffers once per second, calculates &lt;code&gt;BAR_WIDTH&lt;/code&gt; from the current terminal width, and redraws every row in place. TCP samples are each expanded to fill 10 display blocks so the bar fills at the same visual rate as ICMP — without that, a TCP bar would be 90% gray for the first minute and a half.&lt;/p&gt;

&lt;p&gt;State changes (UP→DOWN, DOWN→UP, UP→SLOW) fire a terminal bell and optionally append a timestamped line to a log file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;bash 4+ (macOS ships bash 3 — &lt;code&gt;brew install bash&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ping&lt;/code&gt;, &lt;code&gt;nc&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;mktemp&lt;/code&gt; on PATH&lt;/li&gt;
&lt;li&gt;GNU &lt;code&gt;timeout&lt;/code&gt; / &lt;code&gt;gtimeout&lt;/code&gt; on macOS (&lt;code&gt;brew install coreutils&lt;/code&gt;) — prevents &lt;code&gt;nc&lt;/code&gt; from hanging on unreachable targets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No external dependencies, no compiled components, no containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/pretjeuh/ping-multiple" rel="noopener noreferrer"&gt;github.com/pretjeuh/ping-multiple&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT licensed. Ctrl-C to quit; temp files and cursor state are cleaned up automatically.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>networking</category>
      <category>devops</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Measure bufferbloat on your internet connection with a single Python file</title>
      <dc:creator>pretjeuh</dc:creator>
      <pubDate>Fri, 05 Jun 2026 14:50:43 +0000</pubDate>
      <link>https://dev.to/pretjeuh/measure-bufferbloat-on-your-internet-connection-with-a-single-python-file-3lk9</link>
      <guid>https://dev.to/pretjeuh/measure-bufferbloat-on-your-internet-connection-with-a-single-python-file-3lk9</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That's &lt;strong&gt;bufferbloat&lt;/strong&gt;. And most people have never heard of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What is bufferbloat?&lt;/li&gt;
&lt;li&gt;Why your ISP's speed test won't show it&lt;/li&gt;
&lt;li&gt;What the tool does&lt;/li&gt;
&lt;li&gt;My own results: a D on upload&lt;/li&gt;
&lt;li&gt;Running it — CLI mode&lt;/li&gt;
&lt;li&gt;Running it — Web GUI mode&lt;/li&gt;
&lt;li&gt;How it works under the hood&lt;/li&gt;
&lt;li&gt;The report and what to do about your grade&lt;/li&gt;
&lt;li&gt;What actually fixes bufferbloat&lt;/li&gt;
&lt;li&gt;The deeper problem: AQM isn't a complete solution&lt;/li&gt;
&lt;li&gt;Try it yourself&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What is bufferbloat?
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why large buffers hurt
&lt;/h3&gt;

&lt;p&gt;TCP congestion control works by detecting packet loss: if a packet doesn't arrive, the sender backs off. This mechanism assumes that packets get &lt;em&gt;dropped&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: Active Queue Management
&lt;/h3&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fq_codel&lt;/code&gt;&lt;/strong&gt; (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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAKE&lt;/strong&gt; (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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why your ISP's speed test won't show it
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the tool does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/pretjeuh/bufferbloat" rel="noopener noreferrer"&gt;&lt;code&gt;bufferbloat.py&lt;/code&gt;&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;The five phases are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Baseline&lt;/strong&gt; — measure idle RTT with no load, establishing the true unloaded latency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download&lt;/strong&gt; — saturate the download path with 4 parallel HTTP threads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload&lt;/strong&gt; — saturate the upload path with 4 parallel HTTP POST threads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bidirectional&lt;/strong&gt; — both directions simultaneously, the worst case&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery&lt;/strong&gt; — measure how fast latency returns to baseline after load stops&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"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:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grade&lt;/th&gt;
&lt;th&gt;Added latency under load&lt;/th&gt;
&lt;th&gt;Experience&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;&amp;lt; 5 ms&lt;/td&gt;
&lt;td&gt;Excellent — barely noticeable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;&amp;lt; 30 ms&lt;/td&gt;
&lt;td&gt;Good — minor impact on sensitive apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;&amp;lt; 60 ms&lt;/td&gt;
&lt;td&gt;Moderate — video calls degrade under load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;&amp;lt; 200 ms&lt;/td&gt;
&lt;td&gt;Poor — noticeable lag spikes, games unplayable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;&amp;gt; 200 ms&lt;/td&gt;
&lt;td&gt;Severe — SSH stutters, calls drop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  My own results: a D on upload
&lt;/h2&gt;

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

&lt;p&gt;&lt;strong&gt;Baseline:&lt;/strong&gt; 8ms RTT to Amsterdam&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Download phase:&lt;/strong&gt; 11ms median — 3ms bloat, essentially nothing&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Upload phase:&lt;/strong&gt; 112ms median — &lt;strong&gt;104ms bloat&lt;/strong&gt;, grade D&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Bidirectional:&lt;/strong&gt; 89ms median — 81ms bloat, grade D  &lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;


&lt;h2&gt;
  
  
  Running it — CLI mode
&lt;/h2&gt;

&lt;p&gt;It's a single Python file. On first run it creates its own virtual environment and installs the two dependencies (&lt;code&gt;matplotlib&lt;/code&gt; and &lt;code&gt;flask&lt;/code&gt;) automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/pretjeuh/bufferbloat
&lt;span class="nb"&gt;cd &lt;/span&gt;bufferbloat
python3 bufferbloat.py &lt;span class="nt"&gt;--target&lt;/span&gt; amsterdam
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# See all available targets&lt;/span&gt;
python3 bufferbloat.py &lt;span class="nt"&gt;--list-targets&lt;/span&gt;

&lt;span class="c"&gt;# Use your router as the ping target (measures internal queue separately)&lt;/span&gt;
python3 bufferbloat.py &lt;span class="nt"&gt;--ping-host&lt;/span&gt; 192.168.1.1

&lt;span class="c"&gt;# Longer phases, skip upload (for restricted networks)&lt;/span&gt;
python3 bufferbloat.py &lt;span class="nt"&gt;--target&lt;/span&gt; frankfurt &lt;span class="nt"&gt;--duration&lt;/span&gt; 20 &lt;span class="nt"&gt;--no-upload&lt;/span&gt;

&lt;span class="c"&gt;# Verbose: print each RTT measurement as it arrives&lt;/span&gt;
python3 bufferbloat.py &lt;span class="nt"&gt;--target&lt;/span&gt; amsterdam &lt;span class="nt"&gt;--verbose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tip on ping target choice:&lt;/strong&gt; pinging your router (&lt;code&gt;--ping-host 192.168.1.1&lt;/code&gt;) 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running it — Web GUI mode
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 bufferbloat.py &lt;span class="nt"&gt;--gui&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opens a local Flask web app. Pick a target, watch the RTT chart update live as each phase runs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2nu46xkc46catjz6zjiz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2nu46xkc46catjz6zjiz.gif" alt="Web GUI demo showing live RTT chart" width="719" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

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




&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Load generation
&lt;/h3&gt;

&lt;p&gt;Load uses plain Python — &lt;code&gt;urllib.request&lt;/code&gt; 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 &lt;code&gt;iperf3&lt;/code&gt;, no custom servers, nothing to install beyond the script itself.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Latency measurement
&lt;/h3&gt;

&lt;p&gt;Ping runs once per second via the system &lt;code&gt;ping&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  The GUI stream
&lt;/h3&gt;

&lt;p&gt;The GUI uses Server-Sent Events (SSE) from a Flask &lt;code&gt;/stream&lt;/code&gt; endpoint. Each &lt;code&gt;rtt&lt;/code&gt;, &lt;code&gt;phase_start&lt;/code&gt;, and &lt;code&gt;phase_complete&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Before starting, the &lt;code&gt;/probe&lt;/code&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The report and what to do about your grade
&lt;/h2&gt;

&lt;p&gt;The HTML report is fully self-contained — inline CSS, base64-encoded PNG charts, works offline and is easy to share.&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually fixes bufferbloat
&lt;/h2&gt;

&lt;h3&gt;
  
  
  If you have an OpenWrt router
&lt;/h3&gt;

&lt;p&gt;This is the best case. OpenWrt ships CAKE and fq_codel. Enable SQM (Smart Queue Management):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;opkg update &amp;amp;&amp;amp; opkg install luci-app-sqm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you have DD-WRT
&lt;/h3&gt;

&lt;p&gt;Navigate to &lt;strong&gt;NAT/QoS → QoS&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you have OPNsense or pfSense
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;OPNsense:&lt;/strong&gt; Interfaces → [WAN interface] → Edit → check "Enable Traffic Shaping" and configure HFSC or CAKE via the traffic shaper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pfSense:&lt;/strong&gt; Firewall → Traffic Shaper → HFSC. Alternatively, install the &lt;code&gt;pfSense-pkg-cake&lt;/code&gt; package if available for your version.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you have a consumer router with no AQM support
&lt;/h3&gt;

&lt;p&gt;Your options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enable bridge mode on your ISP modem&lt;/strong&gt; and put a proper router (OpenWrt-capable hardware like a GL.iNet or Belkin RT3200) behind it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flash OpenWrt&lt;/strong&gt; on your existing router if it's supported — check &lt;a href="https://openwrt.org/toh/start" rel="noopener noreferrer"&gt;openwrt.org/toh&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a Raspberry Pi as a router&lt;/strong&gt; — runs Linux natively, full CAKE support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contact your ISP&lt;/strong&gt; — some ISPs will enable AQM on their equipment if asked, especially if you can show them test data&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  If you're on Wi-Fi
&lt;/h3&gt;

&lt;p&gt;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).&lt;/p&gt;




&lt;h2&gt;
  
  
  The deeper problem: AQM isn't a complete solution
&lt;/h2&gt;

&lt;p&gt;Worth knowing: AQM fixes the &lt;em&gt;queue&lt;/em&gt; problem but not all latency spikes under load.&lt;/p&gt;

&lt;p&gt;A 2022 APNIC research paper (&lt;a href="https://blog.apnic.net/2022/01/26/beyond-bufferbloat-end-to-end-congestion-control-cannot-avoid-latency-spikes/" rel="noopener noreferrer"&gt;Beyond Bufferbloat&lt;/a&gt;) 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/pretjeuh/bufferbloat
&lt;span class="nb"&gt;cd &lt;/span&gt;bufferbloat
python3 bufferbloat.py &lt;span class="nt"&gt;--target&lt;/span&gt; amsterdam
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project is open source — &lt;a href="https://github.com/pretjeuh/bufferbloat" rel="noopener noreferrer"&gt;github.com/pretjeuh/bufferbloat&lt;/a&gt;. Issues and PRs welcome — especially if you find a target that doesn't respond reliably, or a platform where ping parsing breaks.&lt;/p&gt;

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

</description>
      <category>python</category>
      <category>networking</category>
      <category>homelab</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
