|
| 1 | +--- |
| 2 | +schema_version: "2026-08-15" |
| 3 | +okf_version: "0.2" |
| 4 | +type: reference |
| 5 | +title: How unispeedtest measures your connection |
| 6 | +description: The sampling plan behind each metric, why throughput uses the 90th percentile, the network quality score thresholds, and how partial failures are handled. |
| 7 | +resource: https://github.com/hsblabs/universal-speedtest-cli |
| 8 | +tags: |
| 9 | + - methodology |
| 10 | + - network |
| 11 | + - speedtest |
| 12 | +status: stable |
| 13 | +hsblabs: |
| 14 | + sidebar: |
| 15 | + label: How it works |
| 16 | + order: 3 |
| 17 | +--- |
| 18 | + |
| 19 | +Speed test numbers only mean something if you know how they were produced. This page documents the sampling plan and the statistics `unispeedtest` applies, so a result can be compared against other tools — or against itself over time — without guessing. |
| 20 | + |
| 21 | +All measurements run against Cloudflare speed test endpoints over a client with a 30 second request timeout. |
| 22 | + |
| 23 | +## Latency |
| 24 | + |
| 25 | +20 samples are taken with a zero-byte `GET` before any load is applied. |
| 26 | + |
| 27 | +Each sample measures the time from request start to the first response byte, then **subtracts Cloudflare's `Server-Timing` value** so the result reflects network round-trip time rather than server processing. When the header is missing or unparseable, the raw client timing is used instead and a warning names how many samples fell back. |
| 28 | + |
| 29 | +- **Unloaded latency** — median of the samples. |
| 30 | +- **Jitter** — mean absolute difference between consecutive samples. Requires at least 2 samples. |
| 31 | +- **Loaded latency** — median of the latency samples collected *during* the download and upload phases, reported separately for each. This is the number that reflects bufferbloat. |
| 32 | + |
| 33 | +## Throughput |
| 34 | + |
| 35 | +Each phase transfers a fixed ladder of payload sizes, small to large, so both fast and slow connections land on a usable set of samples. |
| 36 | + |
| 37 | +| Payload | Download runs | Upload runs | |
| 38 | +| --- | --- | --- | |
| 39 | +| 101 KB | 10 | 8 | |
| 40 | +| 1 MB | 8 | 6 | |
| 41 | +| 10 MB | 6 | 4 | |
| 42 | +| 25 MB | 4 | 4 | |
| 43 | + |
| 44 | +The two directions time different windows, because the byte movement they care about happens at different points in the exchange: |
| 45 | + |
| 46 | +- **Download** — first response byte to last response byte. Connection setup and time-to-first-byte are excluded, so the figure is transfer rate rather than end-to-end request time. |
| 47 | +- **Upload** — request start until the request body has been fully written. If that timing is unavailable, the full end-to-end duration is used instead and a warning is emitted; such samples read slower than the link actually is. |
| 48 | + |
| 49 | +The reported value is the **90th percentile** of those samples, with linear interpolation between neighbours. A high percentile rather than the mean or max: the mean is dragged down by the small payloads that never reach line rate, and the max would report a single lucky burst. |
| 50 | + |
| 51 | +## Packet loss |
| 52 | + |
| 53 | +1000 requests at concurrency 50. The result is the percentage that did not come back, and the terminal report also shows the raw received-over-total count. |
| 54 | + |
| 55 | +## Network quality score |
| 56 | + |
| 57 | +The terminal and HTML reports grade the connection **Good** or **Poor** for three use cases. Every condition must hold for a Good grade: |
| 58 | + |
| 59 | +| Use case | Conditions | |
| 60 | +| --- | --- | |
| 61 | +| Video streaming | download > 5 Mbps, unloaded latency < 100 ms, packet loss < 2% | |
| 62 | +| Online gaming | unloaded latency < 50 ms, jitter < 20 ms, packet loss < 1% | |
| 63 | +| Video chatting | download > 2 Mbps, upload > 2 Mbps, unloaded latency < 100 ms, jitter < 30 ms, packet loss < 1% | |
| 64 | + |
| 65 | +If any of the five inputs is missing, the whole score reads `N/A (insufficient data)` rather than grading on partial evidence. |
| 66 | + |
| 67 | +The score is derived entirely from the metrics above, and is **not** part of the JSON output — a consumer that needs it applies these thresholds itself. |
| 68 | + |
| 69 | +## Failures |
| 70 | + |
| 71 | +The run distinguishes two kinds of failure. |
| 72 | + |
| 73 | +**Fatal** — a phase produced no usable samples at all. Latency, download, upload, and packet loss each abort the run this way. Nothing is reported. |
| 74 | + |
| 75 | +**Partial** — some samples failed, or a derived metric could not be computed. The run continues, the affected field is `null` in JSON, and a warning explains why. Individual sample failures are collapsed into one warning per category carrying the count and the first error. |
| 76 | + |
| 77 | +### Exit codes |
| 78 | + |
| 79 | +| Code | Meaning | |
| 80 | +| --- | --- | |
| 81 | +| `0` | Run completed, or `--version` / `-h` printed and exited | |
| 82 | +| `1` | A measurement phase failed fatally | |
| 83 | +| `2` | Invalid flags, including `-html-title` without `-html` | |
| 84 | + |
| 85 | +A `0` exit does not imply a complete result. Check `warnings` — see [Output formats](./output.md). |
| 86 | + |
| 87 | +## Progress output |
| 88 | + |
| 89 | +Progress lines are written to stdout during a normal run and suppressed by `-json`, so piping to a JSON parser needs no extra redirection. |
0 commit comments