universal-speedtest-cli: Documentation for hsblabs/universal-speedtest-cli. # unispeedtest — command-line internet speed test > A command-line network benchmark that measures download and upload throughput, latency, jitter, and packet loss over Cloudflare speed test endpoints. `unispeedtest` measures the quality of a network connection from the terminal. It currently drives Cloudflare speed test endpoints, and the provider layer is structured so other backends can be added later. ## What it measures [Section titled “What it measures”](#what-it-measures) | Metric | How it is derived | | ------------------- | --------------------------------------------------------------------- | | Download throughput | 90th percentile of sampled Mbps values | | Upload throughput | 90th percentile of sampled Mbps values | | Unloaded latency | Median of 20 samples | | Loaded latency | Measured separately during the download and upload phases | | Jitter | Mean absolute difference between consecutive unloaded latency samples | | Packet loss | 1000 requests at concurrency 50 | | Network metadata | Cloudflare colo, ASN and AS organization, public IP | The reports also grade the connection for streaming, gaming, and video chat. The full sampling plan and the grading thresholds are in [How measurements work](./how-it-works/). ## Install [Section titled “Install”](#install) ### From GitHub Releases [Section titled “From GitHub Releases”](#from-github-releases) The installer script resolves the latest release, verifies the archive against the release `checksums.txt` with SHA-256, and installs to `/usr/local/bin`: ```sh curl -fsSL https://raw.githubusercontent.com/hsblabs/universal-speedtest-cli/main/install.sh | sh ``` Set `INSTALL_DIR` to install elsewhere: ```sh curl -fsSL https://raw.githubusercontent.com/hsblabs/universal-speedtest-cli/main/install.sh | INSTALL_DIR="$HOME/.local/bin" sh ``` To install by hand, download an archive from [Releases](https://github.com/hsblabs/universal-speedtest-cli/releases) and place the `unispeedtest` binary somewhere on your `PATH`. ### With Go [Section titled “With Go”](#with-go) ```sh go install github.com/hsblabs/universal-speedtest-cli/cmd/unispeedtest@latest ``` The binary is named `unispeedtest`. ## Run [Section titled “Run”](#run) ```sh unispeedtest ``` A full run takes roughly a minute and prints a colored summary. Set `NO_COLOR=1` to disable ANSI colors. ## Options [Section titled “Options”](#options) | Flag | Effect | | --------------------- | --------------------------------------------------- | | `-html ` | Write a self-contained HTML report to `` | | `-html-title ` | Append a title to the HTML report; requires `-html` | | `-json` | Print compact single-line JSON | | `-pretty` | Print indented JSON; implies `-json` | | `-v`, `--version` | Print the CLI version and exit | ```sh unispeedtest -html report.html -html-title "Home Wi-Fi" unispeedtest -json unispeedtest -pretty unispeedtest --version ``` Passing `-html-title` without `-html` is an error and exits with status 2. Both report formats are covered in [Output formats](./output/). Under `-json`, progress output is suppressed so stdout stays parseable. # How unispeedtest measures your connection > The sampling plan behind each metric, why throughput uses the 90th percentile, the network quality score thresholds, and how partial failures are handled. 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. All measurements run against Cloudflare speed test endpoints over a client with a 30 second request timeout. ## Latency [Section titled “Latency”](#latency) 20 samples are taken with a zero-byte `GET` before any load is applied. 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. * **Unloaded latency** — median of the samples. * **Jitter** — mean absolute difference between consecutive samples. Requires at least 2 samples. * **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. ## Throughput [Section titled “Throughput”](#throughput) 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. | Payload | Download runs | Upload runs | | ------- | ------------- | ----------- | | 101 KB | 10 | 8 | | 1 MB | 8 | 6 | | 10 MB | 6 | 4 | | 25 MB | 4 | 4 | The two directions time different windows, because the byte movement they care about happens at different points in the exchange: * **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. * **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. 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. ## Packet loss [Section titled “Packet loss”](#packet-loss) 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. ## Network quality score [Section titled “Network quality score”](#network-quality-score) The terminal and HTML reports grade the connection **Good** or **Poor** for three use cases. Every condition must hold for a Good grade: | Use case | Conditions | | --------------- | ----------------------------------------------------------------------------------------------- | | Video streaming | download > 5 Mbps, unloaded latency < 100 ms, packet loss < 2% | | Online gaming | unloaded latency < 50 ms, jitter < 20 ms, packet loss < 1% | | Video chatting | download > 2 Mbps, upload > 2 Mbps, unloaded latency < 100 ms, jitter < 30 ms, packet loss < 1% | If any of the five inputs is missing, the whole score reads `N/A (insufficient data)` rather than grading on partial evidence. 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. ## Failures [Section titled “Failures”](#failures) The run distinguishes two kinds of failure. **Fatal** — a phase produced no usable samples at all. Latency, download, upload, and packet loss each abort the run this way. Nothing is reported. **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. ### Exit codes [Section titled “Exit codes”](#exit-codes) | Code | Meaning | | ---- | ------------------------------------------------------- | | `0` | Run completed, or `--version` / `-h` printed and exited | | `1` | A measurement phase failed fatally | | `2` | Invalid flags, including `-html-title` without `-html` | A `0` exit does not imply a complete result. Check `warnings` — see [Output formats](./output/). ## Progress output [Section titled “Progress output”](#progress-output) Progress lines are written to stdout during a normal run and suppressed by `-json`, so piping to a JSON parser needs no extra redirection. # JSON and HTML output > The JSON payload unispeedtest emits, its nullable fields and warnings, and the self-contained HTML speed test report. `unispeedtest` writes a human-readable summary to stdout by default. `-json` and `-html` add machine-readable and shareable formats; they compose, so a single run can produce all three. ## JSON [Section titled “JSON”](#json) `-json` prints one line; `-pretty` prints the same document indented. ```json { "download_mbps": 225.14, "upload_mbps": 102.87, "latency_ms": { "unloaded": 12.41, "loaded_down": 35.09, "loaded_up": 41.22, "jitter": 1.98 }, "packet_loss_percent": 0.1, "server_colo": "Tokyo", "network_asn": "AS2516", "network_as_org": "KDDI CORPORATION", "ip": "203.0.113.10", "warnings": [ "upload loaded latency unavailable: no samples collected" ] } ``` Progress output is suppressed under `-json`, so stdout is a clean JSON document. ### Partial failures [Section titled “Partial failures”](#partial-failures) A run can succeed overall while an individual measurement fails. When that happens the affected field is emitted as `null` and an explanation is appended to `warnings`, so a consumer can tell missing data apart from a genuine `0`. Two things to handle when parsing: * **Every metric field is nullable.** `download_mbps`, `upload_mbps`, all four `latency_ms` members, `packet_loss_percent`, and the network metadata strings can all be `null`. * **`warnings` is omitted entirely when empty** — it is not an empty array. Read it as “absent or non-empty”. A zero exit code means the run finished, not that every metric is present. See [How measurements work](./how-it-works/) for which failures are fatal and which degrade to a warning. The network quality score shown in the terminal and HTML reports is not included in the JSON. Its thresholds are documented in [How measurements work](./how-it-works/#network-quality-score) if you need to reproduce it. ## HTML report [Section titled “HTML report”](#html-report) `-html <path>` writes a responsive single-file report with no external assets — no CDN, no network access needed to view it. An existing file at the path is overwritten. The measurement time is stored as Unix epoch milliseconds and rendered by inline JavaScript in the viewer’s own locale and time zone, so a report shared across regions reads correctly for each reader. `-html-title` sets a suffix on both the document title and the page heading: ```sh unispeedtest -html report.html -html-title "Home Wi-Fi" ``` produces `Internet Speed Report - Home Wi-Fi`.