Your ISP’s Best Day, Bottled and Sold Back to You
Speedtest.net measures the best-case scenario between your ISP’s peering point and a CDN server that’s been carefully colocated to make everyone look good. It’s speed-testing theatre. You get a big, satisfying number, you screenshot it, you feel validated, and then you wonder why your NAS transfers crawl at 300 Mbps on a “gigabit” network.
Honestly, Ookla’s interface has more ads than a 2009 Flash game site. You click “Go,” wait through a banner ad, watch a spinner, and get a number that means roughly nothing for your actual use case.
LibreSpeed fixes this. It’s an open-source, self-hosted speed test server that measures what you actually care about: LAN throughput, VPN tunnel bandwidth, jitter, packet loss, and real ISP performance from your actual network path. No ads. No phoning home. Just numbers.
What LibreSpeed Actually Measures (and Why That’s Useful)
LibreSpeed uses HTTP to simulate upload and download. That means it respects your actual application-layer stack (TCP, your kernel’s buffer settings, your NIC driver) rather than ICMP or raw UDP blasting past all that.
Where it shines:
- LAN diagnosis: Is your 2.5G NAS actually delivering 2.5G, or is it capping at 940 Mbps because the switch port is negotiating wrong?
- VPN tunnel throughput: Tailscale, WireGuard, OpenVPN, they all impose overhead. How much? Put a LibreSpeed instance on the far side of the tunnel and find out.
- True ISP measurement: Host it on a VPS. Now you’re measuring from your home router to a server you control, not a CDN that Comcast has a peering agreement with.
- Historical tracking: Enable telemetry, cron the CLI, graph it in Grafana. Watch your ISP throttle after your FUP cap kicks in on day 22 of the billing cycle.
Deploy It: Docker Compose
LibreSpeed ships as adolfintel/speedtest. The image is small, the config is environment variables, and the whole thing is up in under a minute.
services: speedtest: image: adolfintel/speedtest:latest container_name: librespeed restart: unless-stopped environment: MODE: standalone TELEMETRY: "true" ENABLE_ID_OBFUSCATION: "true" PASSWORD: "changeme" # admin login for results viewer IPINFO_APIKEY: "" # optional: ipinfo.io key for geolocation TIME_DL_S: "15" # download test duration (seconds) TIME_UL_S: "15" # upload test duration (seconds) MAX_THREADS: "6" # parallel streams for 10G testing DISTANCE: "km" ports: - "8080:80" volumes: - librespeed_db:/database
volumes: librespeed_db:docker compose up -dHit http://<your-host>:8080 and you’ve got a clean speed test UI with zero ads.
Environment Variables That Matter
| Variable | Default | What it does |
|---|---|---|
MODE | standalone | standalone serves everything from one container |
TELEMETRY | false | Enable SQLite result logging |
PASSWORD | (none) | Admin password for /results/stats.php |
TIME_DL_S | 15 | Download test duration in seconds |
TIME_UL_S | 15 | Upload test duration |
MAX_THREADS | 6 | Parallel HTTP streams, increase for 10G |
IPINFO_APIKEY | (none) | ipinfo.io API key for client geolocation |
For a 10G home lab, bump MAX_THREADS to 8 or 10. A single HTTP stream will saturate at around 3 to 4 Gbps on most commodity hardware; you need multiple parallel streams to push a 10G link. Set TIME_DL_S and TIME_UL_S to at least 20 to let the test ramp up and stabilize.
Testing Your LAN
Deploy LibreSpeed on the same machine as your NAS, or on a dedicated server on the same switch. Then open the URL from any device on the LAN.
This tells you:
- Whether your NIC is actually linked at the right speed (a 1G link pretending to be 2.5G is depressingly common)
- Whether your switch is the bottleneck
- Whether wireless clients are actually getting the throughput their spec claims
A 2.5GbE NAS link should deliver ~2.2 to 2.35 Gbps in practice. If you’re seeing 940 Mbps, check ethtool eth0 on the NAS and the switch port config. LibreSpeed just told you something was wrong. Without LibreSpeed, you’d still be wondering why Plex transcodes instead of direct plays.
Testing Across a VPN Tunnel
This is the use case that actually gets me excited. Put LibreSpeed on a node that’s accessible via your VPN: a Tailscale-connected server, a WireGuard peer, a Headscale node.
For Tailscale specifically, deploy LibreSpeed on any node in your tailnet, then test from another. The number you get is your actual tunnel throughput, accounting for WireGuard encryption overhead, MTU, and CPU speed on both ends.
# On the Tailscale node running LibreSpeeddocker compose up -d
# From another Tailscale device, open:# http://<tailscale-ip>:8080You’ll find that:
- DERP-relayed connections (when direct connection fails) tank your throughput
- CPU-limited nodes (Raspberry Pi, cheap VPS) hit encryption ceilings around 500 Mbps
- Direct UDP connections get you much closer to the underlying link speed
This is the fastest way to diagnose “why is my VPN slow” without guessing.
The CLI Version: librespeed-cli
For cron jobs, scripting, and Grafana ingestion, there’s librespeed-cli, a Go binary that speaks the LibreSpeed protocol.
# Install (Linux amd64)curl -LO https://github.com/librespeed/speedtest-cli/releases/latest/download/librespeed-cli_linux_amd64.tar.gztar -xzf librespeed-cli_linux_amd64.tar.gzsudo mv librespeed-cli /usr/local/bin/
# Run against your self-hosted server (server list passed as JSON via stdin)echo '[{"id":1,"name":"home","server":"http://192.168.1.50:8080/backend/","dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php"}]' \ | librespeed-cli --local-json - --server 1 --simple
# CSV output for loggingecho '[{"id":1,"name":"home","server":"http://192.168.1.50:8080/backend/","dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php"}]' \ | librespeed-cli --local-json - --server 1 --csvHeads up: --server-json/--local-json expect a server list JSON (with dlURL, ulURL, pingURL, getIpURL fields relative to server), not a direct backend URL. Pass it inline via stdin with --local-json -, or save it to a file and point --local-json at that. The --simple flag gives you clean one-line output: download, upload, ping, jitter.
For cron-based logging:
*/30 * * * * root /usr/local/bin/librespeed-cli \ --local-json /etc/librespeed/servers.json --server 1 \ --csv >> /var/log/speedtest.csv 2>&1(Drop your server-list JSON at /etc/librespeed/servers.json so cron has a file to read, stdin piping is awkward in a crontab line.)
Now you have timestamped CSV data every 30 minutes. Ingest that into Prometheus via a CSV exporter, or import directly into Grafana as a CSV data source. You’ll have a “my ISP is throttling me at peak hours” dashboard before the week is out.
Telemetry: Actually Useful for Once
Enable TELEMETRY=true and the container logs every test result into a SQLite database at /database/telemetry.db. The admin interface lives at:
http://<your-host>:8080/results/stats.phpLog in with the PASSWORD you set. You’ll see a table of every test run: timestamp, IP, download, upload, ping, jitter, ISP name (if you set an IPINFO_APIKEY).
For Grafana integration, you can either:
- Mount the SQLite file and use Grafana’s SQLite plugin to query it directly
- Export CSV from the admin UI and import as a data source
The SQLite schema is simple:
SELECT timestamp, ip, ispinfo, dl, ul, ping, jitterFROM speedtest_telemetryORDER BY timestamp DESCLIMIT 100;dl and ul are in Mbps. Plug that into a Grafana time series and you’ve got a real ISP monitoring dashboard that costs you nothing and leaks nothing to Ookla.
Reverse Proxy: HTTPS with Caddy or Traefik
Running this on your internal network? Fine without HTTPS. Exposing it to the internet or over Tailscale to multiple users? Add HTTPS.
Caddy
speedtest.yourdomain.com { reverse_proxy librespeed:80}services: speedtest: image: adolfintel/speedtest:latest container_name: librespeed restart: unless-stopped environment: MODE: standalone TELEMETRY: "true" PASSWORD: "changeme" volumes: - librespeed_db:/database networks: - caddy_net
caddy: image: caddy:2-alpine restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data networks: - caddy_net
volumes: librespeed_db: caddy_data:
networks: caddy_net:Traefik Labels
If you’re already running Traefik:
services: speedtest: image: adolfintel/speedtest:latest container_name: librespeed restart: unless-stopped environment: MODE: standalone TELEMETRY: "true" PASSWORD: "changeme" labels: - "traefik.enable=true" - "traefik.http.routers.librespeed.rule=Host(`speedtest.yourdomain.com`)" - "traefik.http.routers.librespeed.entrypoints=websecure" - "traefik.http.routers.librespeed.tls.certresolver=letsencrypt" - "traefik.http.services.librespeed.loadbalancer.server.port=80" volumes: - librespeed_db:/database
volumes: librespeed_db:LibreSpeed vs iperf3: Pick Your Tool
These aren’t competitors. They measure different things.
iperf3 is raw TCP/UDP throughput. It bypasses the HTTP stack, so it tells you what your hardware can theoretically push. It’s great for:
- Characterizing a new switch or NIC
- Measuring raw link capacity
- Testing UDP at specific bitrates
But iperf3 doesn’t measure jitter, packet loss in a web-realistic context, or anything you’d recognize as a user-facing metric.
LibreSpeed uses HTTP. It gives you:
- Jitter (how consistent is the latency?)
- A result that reflects actual browser/app behavior
- History and visualization
- Something non-technical people can run without a CLI
Use iperf3 when you’re commissioning new hardware. Use LibreSpeed day-to-day. They live in the same toolbox.
# iperf3: raw TCP throughput (server on one machine)iperf3 -s
# iperf3: client test (from another machine)iperf3 -c 192.168.1.50 -t 30
# LibreSpeed CLI: user-facing metricslibrespeed-cli --local-json /etc/librespeed/servers.json --server 1Calibrating for 10G
Default settings are tuned for typical 100M to 1G home connections. For 10G testing, you need to tune:
environment: MAX_THREADS: "10" # More parallel HTTP streams TIME_DL_S: "20" # Longer to let TCP ramp up TIME_UL_S: "20" CHUNK_SIZE: "100" # Larger chunks (MB) per requestYou also need the host running LibreSpeed to have enough CPU for multi-threaded HTTP. On a 10G test, you’re pushing ~1.25 GB/s through an HTTP server. A Raspberry Pi 4 will not do this. A modest x86 box (quad-core, 8+ GB RAM) handles it fine.
Check that your NIC isn’t bottlenecked by interrupt affinity. If you’re hitting a ceiling at ~6 Gbps and your hardware should handle more, irqbalance or manual CPU pinning can help.
The Browser UX (It’s Actually Good)
Open LibreSpeed in a browser and you get a clean, single-page interface: big dial, download/upload/ping/jitter metrics, no banner ads, no cookie consent wall, no “upgrade to Pro” nag. It looks like what speedtest.net used to look like in 2015 before it became a landing page for enterprise SaaS.
The interface is responsive, works on mobile, and loads fast. It’s not exciting, which is exactly what a diagnostic tool should be.
Should You Bother?
If you’re running a home lab with a NAS, multiple VLANs, a VPN into your network, or you’re paying for gigabit and wondering if you’re actually getting it: yes, absolutely bother.
Setup takes about five minutes. The ongoing maintenance is zero. You get:
- Honest throughput numbers for your actual infra, not Ookla’s marketing stats
- A CLI tool you can cron and graph
- A result history you own and control
- No ads, no data collection, no Ookla account
The one time you use it to diagnose a misconfigured switch port or confirm your VPN isn’t the bottleneck you thought it was, it pays for the five minutes you spent deploying it. Probably ten times over.
Speedtest.net is fine for “is the internet working.” LibreSpeed is for people who want to know why it’s working, or why it isn’t.
Run it. Point it at your infra. See what’s actually happening on your network.