A Self-Hosted Speedtest That Keeps History

One speedtest tells you nothing; a year of them tells you whether to phone the ISP

Contents

The support script is always the same. You phone the ISP because the line has been dreadful all week, and the person on the other end asks you to run a speedtest, right now, while they wait. You do. It comes back fine — 480 down, near enough the 500 you pay for — because at 2pm on a Tuesday the line is fine. The problem lives at 8pm, when the whole street is streaming and your connection quietly collapses to 60. The one-off test you just ran has, if anything, made your case worse.

A single speedtest is a photograph of one instant. What you actually need to argue with an ISP, or just to understand your own connection, is a film: the same measurement taken every hour for weeks, so the daily collapse shows up as a pattern nobody can wave away. That’s the entire premise of a self-hosted speedtest with history. Run the test on a schedule, store every result, graph the lot, and the evening slowdown stops being your subjective impression and becomes a chart with timestamps on it.

Why history beats a bigger number

Advertisement

The instinct is to chase the peak figure — to run the test over and over until you catch a good number and feel reassured. That’s the least useful thing the data can tell you. A broadband connection’s headline speed is the easy part; almost every line hits close to its rated speed at 3am. The number that determines whether your evenings are miserable is the floor under load, and the consistency across the day. Neither is visible in a single run.

With a month of hourly samples you can answer the questions that matter. Does the line degrade at a predictable time, which points at congestion on the ISP’s side? Is the degradation random and spiky, which points at a flaky modem, a marginal cable, or interference? Did last Thursday’s engineer visit actually change anything, or did the graph carry on exactly as before? Is your “500 Mbit” connection delivering a median of 470 or a median of 210? These are questions about distributions over time, and no amount of staring at a live gauge answers them.

There’s a second, quieter benefit. When you keep the history yourself, you own the evidence. The ISP’s own view of your line is not something they’ll share, and the speedtest.net result you ran during the call vanishes the moment you close the tab. Your own graph, going back months, is a document. It has settled more than one dispute in my experience by turning “it feels slow sometimes” into “here is the median throughput between 8 and 11pm, every night for six weeks”.

WAN or LAN? Decide what you’re actually measuring

Before you install anything, get clear on what question you’re asking, because there are two completely different speedtests and confusing them wastes weeks.

A WAN speedtest measures your connection to the internet — your machine, through your router, through the modem, out to a distant test server and back. This is the one that reflects what you pay your ISP for. It’s what Ookla’s speedtest.net does, and it’s what you want for the ISP-argument use case. The tooling here drives the official Ookla CLI or a compatible client against public servers.

A LAN speedtest measures throughput between two machines inside your own network — say your desktop and your NAS. This tells you about your switches, your cabling, and your Wi-Fi, and it says nothing whatever about your ISP. A self-hosted LibreSpeed instance, with the server on one box and the browser on another, measures this internal path. It’s genuinely useful for finding a duff cable or a Wi-Fi dead spot, and it complements per-device bandwidth monitoring on a managed switch, but don’t mistake a 940 Mbit LAN result for proof your internet is healthy.

For “is my ISP delivering”, you want scheduled WAN tests. For “why is this one machine slow copying to the NAS”, you want a LAN test. This article is about the first; I’ll flag where the second sneaks in.

The easy path: speedtest-tracker

Advertisement

The lowest-effort setup that keeps history is speedtest-tracker, a self-hosted app that wraps the Ookla CLI, runs it on a cron schedule you set, stores every result in a database, and gives you a web dashboard with graphs of download, upload, ping and jitter over time. It runs in a single container. A minimal compose file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# docker-compose.yml
services:
  speedtest-tracker:
    image: lscr.io/linuxserver/speedtest-tracker:latest
    container_name: speedtest-tracker
    restart: unless-stopped
    ports:
      - "8080:80"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/London
      - APP_KEY=base64:REPLACE_WITH_php_artisan_key_generate
      - DB_CONNECTION=sqlite
      - SPEEDTEST_SCHEDULE=0 * * * *      # top of every hour
      - SPEEDTEST_SERVERS=              # blank = auto-pick nearest
    volumes:
      - ./data:/config

docker compose up -d, open the dashboard on port 8080, and within the hour you have your first data point, with a fresh one every hour after. The SPEEDTEST_SCHEDULE is a standard cron expression — hourly is a sensible default and I’ll explain why below. The TZ line matters more than it looks: get the timezone wrong and your “8pm slowdown” lands at the wrong place on the graph, which is exactly the confusion you were trying to remove.

One wrinkle worth knowing: the Ookla CLI requires you to accept its licence and GDPR terms on first run. The container images generally handle this by passing an acceptance flag, but if your very first tests fail with a licence prompt in the logs, that’s the cause, and it’s a one-time thing.

The DIY path: cron, the CLI, and Influx

If you already run Grafana and would rather have speedtest results sitting alongside your other dashboards, skip the dedicated app and push results into your existing time-series database. The pattern is a small script on a cron timer that runs the CLI in JSON mode and writes the numbers to InfluxDB in line protocol:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/env bash
# /usr/local/bin/speedtest-to-influx.sh
set -euo pipefail

# --format=json gives machine-readable output; --accept-license is one-and-done
result=$(speedtest --accept-license --accept-gdpr --format=json)

# Ookla reports bandwidth in BYTES per second; ×8 for bits, ÷1e6 for Mbit
dl=$(echo "$result" | jq '.download.bandwidth * 8 / 1000000')
ul=$(echo "$result" | jq '.upload.bandwidth   * 8 / 1000000')
ping=$(echo "$result" | jq '.ping.latency')
srv=$(echo "$result" | jq -r '.server.name')

curl -sS -XPOST 'http://influxdb:8086/api/v2/write?org=mylab&bucket=network' \
  -H "Authorization: Token ${INFLUX_TOKEN}" \
  --data-binary "speedtest,server=${srv// /_} download=${dl},upload=${ul},ping=${ping}"

Then a cron line:

1
2
# /etc/cron.d/speedtest
0 * * * *  root  /usr/local/bin/speedtest-to-influx.sh >> /var/log/speedtest.log 2>&1

The single detail that trips everyone up is the units. Ookla’s JSON reports bandwidth in bytes per second, so a 500 Mbit line reports roughly 62500000. Multiply by 8 to get bits, divide by a million for Mbit, and your graph will read the way the marketing does. Skip that and you’ll spend an afternoon convinced your gigabit line is running at 118 Mbit. Once the numbers land in Influx, a Grafana panel with a time-series of download and a stat panel showing the daily minimum gives you everything the dedicated app does, in a dashboard you can park next to your Uptime Kuma status page and whatever host metrics you already collect with something like Netdata.

Scheduling: resist the urge to test constantly

Every time I set this up, the temptation is to test every five minutes for a “detailed” picture. Don’t. There are three concrete reasons, and they compound.

First, a speedtest is not a passive measurement — it saturates your line on purpose. That’s how it works: it floods the connection to find the ceiling. Run it every five minutes and you’re regularly making everyone else’s video call stutter, and worse, you’re polluting your own per-device bandwidth graphs with a big periodic spike that was you, testing. Second, each full test moves a meaningful chunk of data — often a gigabyte or more on a fast line. Twelve tests an hour on a metered or capped connection is a real bill. Third, and most importantly, five-minute resolution tells you nothing that hourly resolution doesn’t. Congestion patterns play out over hours, so hourly sampling captures the shape of the day perfectly while staying invisible to everyone else on the network.

Hourly is my default. If you specifically want to nail down an evening slowdown, tighten to every fifteen or thirty minutes for the 6pm–midnight window and leave the rest of the day hourly. Cron handles that with two schedule lines. There’s no case in a home setting for testing more often than that.

Troubleshooting

Results swing wildly between runs with nothing else changed. The usual culprit is server selection. If the CLI auto-picks the nearest server each time, it may hop between a well-connected server and an overloaded one, and the “speed change” you’re seeing is really a server change. Pin the test to one or two specific, reliable servers by ID so that run-to-run comparisons are honest. In speedtest-tracker that’s the SPEEDTEST_SERVERS setting; on the CLI it’s --server-id.

The test never reaches your rated speed even at 3am. Check what’s running the test. A Raspberry Pi or a low-power NAS may not have the CPU, or the crypto throughput, to saturate a gigabit line single-threaded, so you’re measuring the test host rather than the connection. Run it from a capable machine on a wired connection before concluding the line is slow. And if the host is on Wi-Fi, you’re measuring Wi-Fi — put the tester on Ethernet for any result you intend to quote at an ISP.

Every result is a little low and you can’t work out why. Something else on the network is using bandwidth during the test — a backup, a cloud sync, a games update — and it’s stealing headroom from the measurement. Scheduled tests inevitably sometimes collide with scheduled backups. This is a real limitation of black-box active testing; it’s why you look at the median and the shape over weeks rather than trusting any single data point.

The graph’s daily pattern is shifted by an hour or several. Timezone mismatch between the container/cron environment and where you’re reading the graph, made worse twice a year by daylight saving. Set TZ explicitly everywhere and confirm a known test time lands where you expect.

First tests fail with a licence or GDPR message in the logs. The Ookla CLI wants explicit acceptance on first run. Pass --accept-license --accept-gdpr (or the app’s equivalent environment flag) once and it stops asking.

Is it worth it?

For anyone who has ever suspected their ISP of under-delivering and lacked the evidence to say so, this is an afternoon’s work that pays for itself the first time you need it. The dedicated app route is genuinely fifteen minutes to a working dashboard, and the DIY route is worth it if you already run Grafana and want everything in one place.

Keep the expectations honest, though. Active speedtesting is a blunt instrument: it saturates the line to measure it, it’s sensitive to server choice and to what else is running, and any single result can be misleading for a dozen mundane reasons. Its value is entirely in the aggregate — weeks of samples turning a vague complaint into a distribution you can point at. If you want continuous, passive insight into what’s actually using the connection moment to moment, that’s a different tool, the switch port counters, and the two work well together: the speedtest history tells you the line is slow at 8pm, and the port graphs tell you whether it’s the ISP or your own housemate’s download. For the price of one small container and a cron line, having your own months-long record of what you’re paying for is the sort of quiet leverage every homelab should keep on hand.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.