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)
- Bufferbloat grade — latency is sampled while the link is saturated and graded A+–F against idle, per direction — the number Ookla's CLI doesn't show you
- Four providers — LibreSpeed (default), Ookla speedtest.net, Netflix Fast.com, and
customfor your own servers (self-hosted backends, LAN testing); switch with--provideror config - Client and server in one binary —
netspd serve+netspd --urlmeasure pod-to-pod, host-to-host or LAN links with nothing else installed; auto-headless in containers, env-var flags for K8s manifests - Smart server discovery — health-probes the server list, drops dead servers, auto-selects the nearest
- Headless mode —
--no-tuifor scripts and cron,--json/--csvfor machine-readable reports - Shareable results —
ycopies a paste-anywhere result card to the clipboard - 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; mouse wheel and click work in lists
- Plays nice everywhere —
--asciifor fonts without braille/blocks,NO_COLORrespected,-4/-6to force an address family - 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).
With --interval (or repeat_interval in config) the TUI shows a countdown on the results screen and re-runs automatically — leave it open as a dashboard while the trends accumulate. Headless, the interval turns netspd into a watch loop that logs every run and survives transient failures, ideal under systemd or in a tmux pane.
Keyboard
| Key | Action |
|---|---|
q / Esc |
Quit (Esc closes overlays first) |
r |
Restart the test |
y |
Copy a shareable result card to the clipboard |
g |
Result trends from your history |
s |
Server selection |
t |
Theme selector |
c |
Edit configuration (←→ adjust, w save) |
? |
Help |
↑↓ / jk, Enter |
Navigate and confirm in lists (mouse wheel + click too) |
←→ / hl |
Adjust values · filter trends by server |
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.
Containers and Kubernetes
netspd is a single static binary that plays both roles: client and server. That makes pod-to-pod and pod-to-internet testing a two-liner — no separate backend image.
Add it to any image (the musl binary has zero runtime dependencies):
ARG NETSPD_VERSION=v0.1.3
ADD https://github.com/TarunVishwakarma1/netspd/releases/download/${NETSPD_VERSION}/netspd-${NETSPD_VERSION}-x86_64-unknown-linux-musl.tar.gz /tmp/
RUN tar -xzf /tmp/netspd-*.tar.gz --strip-components=1 -C /usr/local/bin --wildcards '*/netspd' && rm /tmp/netspd-*.tar.gz
When no terminal is attached (containers, CI, pipes), netspd automatically runs headless — no flags needed. Every flag also reads an env var (NETSPD_URL, NETSPD_SERVER, NETSPD_PROVIDER, NETSPD_INTERVAL, NETSPD_JSON, …).
Pod ↔ pod throughput — run the built-in server in one pod:
# server pod
containers:
- name: netspd-server
command: # listens on :9516
ports:
# expose it as a Service named netspd-server
and point any other pod at it:
# client Job / debug container
command:
env:
-
-
The JSON report (download/upload Mbps, ping, jitter, bufferbloat grade) lands in the pod logs, ready for any log pipeline. Loopback sanity: the loop sustains 11+ Gbps down / 22+ Gbps up, so the tool won't be the bottleneck on cluster networks.
Pod → internet — same image, no server needed; a CronJob makes it a scheduled monitor:
apiVersion: batch/v1
kind: CronJob
metadata:
spec:
schedule: "*/30 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: netspd
image: your-image-with-netspd
command:
netspd serve also answers LibreSpeed paths (empty.php, garbage.php), so existing LibreSpeed clients can test against it too. ICMP loss needs CAP_NET_RAW in containers and degrades gracefully without it.
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 (
--interval)