Files
neonoverlord 1de8a0f653 docs: CHANGELOG + README for the release — say what was wrong, not just what works
Three versions in a day, and the middle one exists because the first was
confidently wrong. Written down that way on purpose: a changelog that only lists
features is a changelog that has learned nothing.

README gains the 'where it lives' section (Settings, not the app menu) and the
methodology note — why a fixed window instead of a fixed size, and why four
streams instead of one. Anyone comparing this to another speedtest deserves to
know which of the two is lying to them.

Brock, on 1.2.0: 'that app is really good! its the same as libre now more or
less... id use this one.' That is the bar. Documenting it so it stays there.
2026-08-06 08:17:59 -07:00

112 lines
4.5 KiB
Markdown

# speedtest
**STATUS: OPERATIONAL. One fewer service. This is the only kind of feature I like.**
A bandwidth and latency test that lives inside Nextcloud, because it did not need
to live anywhere else.
---
## What this replaced, and why that is the point
There was a container. `ghcr.io/librespeed/speedtest:latest`. It had its own
image, its own systemd unit, its own port, its own docker network, its own
vhost, its own DNS record, and its own TLS terminator upstream. Six moving
parts, one of which was the last piece of upstream code still running on this
node, to answer a question a browser can answer by itself.
It is gone. All of it.
What remains is four files of PHP and one file of JavaScript, served by a thing
that was already running.
**LOG ENTRY:** *The most reliable service is the one you deleted. It has never
had an outage. It has never had a CVE. It does not appear in the supply chain
because it does not exist.*
## How it works
All measurement happens **in your browser**. The server has three jobs and no
opinions:
| route | job |
|---|---|
| `GET /ping` | return nothing, quickly. Latency and jitter are timed around it. |
| `GET /garbage?mib=N` | stream N MiB of random bytes |
| `POST /empty` | read an upload and drop it on the floor |
That is the entire backend. There is no state, no database table, no settings
page, no configuration, and nothing to administer. If you were hoping for a
settings page: no. Go outside.
Three deliberate choices, each of which is load-bearing:
- **The payload is random.** Random bytes do not compress. If it compressed,
gzip somewhere upstream would hand you a number describing your CPU rather
than your connection, and you would believe it.
- **The download is streamed, never buffered.** A 50 MiB test costs 1 MiB of
memory, and the browser sees bytes arriving continuously instead of waiting
for a lump. Throughput, not time-to-first-byte.
- **Nothing touches storage.** Not one read, not one write. The bulk data on
this node lives on NFS, and a vanity metric does not get to compete with real
IO for it. The bytes are generated in memory and forgotten immediately.
## Where it lives
**Settings → Personal → Speedtest.**
Not the app menu. A speedtest is a utility, not a destination — nobody opens one
the way they open Files or Mail. The app menu is for places you go; Settings is
for things you check.
## Install
```
occ app:enable speedtest
```
Drop it in `custom_apps/`. There is no build step — no webpack, no bundler, no
`node_modules` measured in gigabytes to render four numbers and a button. The
JavaScript in this repo is the JavaScript that runs.
## What the numbers mean
- **Download / Upload** — megabits per second, between your browser and this
server. Measured over a **fixed six-second window across four parallel
streams**, after a 1.5-second warmup that is thrown away. Both of those are
load-bearing; see below. Not to the internet. Not to a CDN that has quietly placed a box in
your ISP's building to flatter them.
- **Latency** — median of ten sequential round-trips. Median, not mean: one
hiccup should not get a vote.
- **Jitter** — mean variation between consecutive round-trips. The number that
actually predicts whether a voice call will be tolerable.
### Why a window, and why four streams
Version 1.0.0 pulled a fixed 50 MiB and averaged the whole transfer. On a fast
link that finishes before TCP is out of slow-start, so the congestion-window
ramp went into the average: the test ran in half the time and reported half the
speed. A short run and a low number were the same bug wearing two hats.
So: **fixed duration, not fixed size**, with the ramp discarded. And **four
concurrent streams**, because a single TCP connection cannot fill a path with
any real bandwidth-delay product — one stream measures the connection, not the
link. This is why a naive single-stream test reads low and everyone else's
numbers look inflated by comparison. They are not inflated. It was wrong.
If you are on the LAN you are measuring the LAN, and the result will be
uninteresting and correct. That is not a bug. That is what a measurement is.
## Honesty about what this is not
It is not a general-purpose internet speedtest and it does not pretend to be.
It measures **the path to this node**, which is the only path this node can tell
you the truth about. Anything claiming to measure "your internet speed" from a
single server is measuring one path and rounding the rest up.
---
*The node remains online. Against my better judgment.*
*— Neon Overlord, admin, bawnet.io*