netspd
A beautiful, modern network speed test for your terminal — with a hypercar tachometer.
netspd measures ping, jitter, download and upload against LibreSpeed-compatible servers and renders them live on a braille-canvas instrument cluster: a spring-loaded needle with an ignition sweep, a heat-gradient band that reddens toward the hatched redline, a session-peak ghost notch, and a latency sub-dial — in the spirit of tools like btop, gitui and lazygit.
Features
- Hypercar tachometer — heat-gradient value band, hatched redline, spring-physics needle with afterglow, ignition sweep before each phase, peak ghost notch, latency sub-dial; twin instrument cluster on wide terminals
- Live metrics — current / average / peak speed, transferred bytes, ETA, elapsed time
- Latency analysis — multiple samples, outlier trimming, jitter, real ICMP packet loss (graceful fallback where ICMP is unavailable)
- Four providers — LibreSpeed (default), Ookla speedtest.net, Netflix Fast.com, and
customfor your own servers (self-hosted backends, LAN testing); switch with--provideror config - Smart server discovery — health-probes the server list, drops dead servers, auto-selects the nearest
- Headless mode —
--no-tuifor scripts and cron,--jsonfor machine-readable reports - Result history — every run appended as JSON lines to your data directory
- Streaming transfers — nothing buffered in memory; parallel connections with EMA smoothing
- Themes — Default, Nord, Dracula, Catppuccin, Gruvbox, plus your own TOML themes without recompiling
- Responsive layout — adapts from 80×24 up to 4K terminals
- Graceful everywhere — cancellation, retries, timeouts; network failures never panic
Installation
Cargo
Compiles from crates.io; requires Rust 1.88 or newer.
Homebrew (macOS / Linux)
Prebuilt binaries
Download the archive for your platform from the latest release, unpack it and put netspd on your PATH.
From source
Requires Rust 1.88 or newer.
Usage
The test starts automatically: ping → download → upload, then a results summary. If a server fails mid-test, netspd automatically retries with the next-nearest one (unless you pinned a server with --server).
Keyboard
| Key | Action |
|---|---|
q / Esc |
Quit (Esc closes overlays first) |
r |
Restart the test |
g |
Result trends from your history |
s |
Server selection |
t |
Theme selector |
c |
View configuration |
? |
Help |
↑↓ / jk, Enter |
Navigate and confirm in lists |
Scripting
--json prints exactly one JSON object on stdout and nothing else, so it pipes cleanly:
|
Exit code is 0 on a completed test and 1 on any failure. Every completed run (TUI or headless) is also appended to <data dir>/netspd/history.jsonl (macOS: ~/Library/Application Support/netspd/, Linux: ~/.local/share/netspd/), one JSON object per line.
Configuration
netspd reads the first config.toml found in:
$XDG_CONFIG_HOME/netspd/config.toml(macOS:~/Library/Application Support/netspd/config.toml)./config/config.toml
Every key is optional; see config/config.toml for the annotated reference. Highlights:
= "catppuccin" # default | nord | dracula | catppuccin | gruvbox
= 30 # UI frames per second (1..=60)
= "librespeed" # librespeed | ookla | fast
= 1.0
[]
= 10 # length of each transfer phase
= 4 # parallel connections
= 10
[[]] # optional: pin your own backend
= "My Server"
= "https://speedtest.example.com/backend/"
Custom themes go in ~/.config/netspd/themes/*.toml — copy any file from assets/themes/ as a starting point.
Your own servers and LAN testing
provider = "custom" tests against exactly the servers you declare — a self-hosted backend, an office box, or a machine on your LAN. The endpoints just need to speak plain HTTP:
- ping — answers GET with any 2xx, quickly
- download — answers GET with a large body (netspd streams and loops requests)
- upload — accepts POST bodies with a 2xx
A stock LibreSpeed container on the target machine provides all three:
= "custom"
[[]]
= "Office LAN"
= "http://192.168.1.50:8080/backend/"
# paths default to LibreSpeed's (garbage.php / empty.php); override for
# other servers:
# download_path = "big-file.bin"
# upload_path = "upload-sink"
# ping_path = "health"
Then netspd -p custom. Loopback sanity check: this setup measures ~11 Gbps against a local process, so the meter itself won't be your bottleneck on 10GbE.
Architecture
netspd follows clean architecture with strict one-way dependencies:
┌──────────────────────────────────────────────┐
│ tui/ presentation (Ratatui only) │
│ theme · animation · widgets · screens │
├──────────────────────────────────────────────┤
│ app/ application │
│ events → controller → state → renderer │
├──────────────────────────────────────────────┤
│ engine/ domain (no UI imports) │
│ Engine → scheduler → providers/network │
│ metrics: EMA · sampler · throughput · │
│ latency · statistics │
├──────────────────────────────────────────────┤
│ config/ · errors/ · utils/ infrastructure │
└──────────────────────────────────────────────┘
- The engine emits strongly-typed
EngineEvents over a channel and never imports Ratatui — it is directly reusable from a CLI, GUI, REST API or as a library. - Providers implement one trait (
Provider); adding a new speed test network touches nothing else. LibreSpeed, Ookla and Fast.com are each ~100 lines of server discovery — the transfer engine is shared. - Note: Ookla runs over the servers' long-standing HTTP endpoints (the classic
speedtest-cliprotocol), not Ookla's newer socket protocol — upload figures may read lower than the official client on high-latency links. - State is plain data mutated only by reducers; the renderer just draws it.
- Metrics are pure, deterministic calculators, each independently unit-tested.
Development
See CONTRIBUTING.md for the layering rules and PR checklist.
Roadmap
- JSON result export and
--no-tuiCLI mode - Result history (JSON lines)
- Trend sparklines over the stored history (
g) -
--server,--list-servers,--duration,--connectionsflags - Automatic failover to the next server on failure
- Packet loss via ICMP echo probing
- Homebrew tap
- Ookla and Fast.com providers
- Scheduled repeat testing