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
- Plain-language verdict — every result explains itself: "Good for 4K streaming · video calls may stutter under load"
- Monitoring-ready — Prometheus textfile output (
--prom-textfile), threshold exit codes (--fail-below), JSON/CSV, history - 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
- Wallpaper — set
kind = "gradient"under[wallpaper]inconfig.tomlfor a vertical colour gradient behind the UI - 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.
Other channels
- Nix:
nix run github:TarunVishwakarma1/netspd - Debian/RPM:
.deband.rpmfiles on the releases page - Scoop / winget / AUR: ready-to-submit manifests in
packaging/
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 |
u |
Toggle speed unit — Mbps ↔ MB/s |
y |
Copy a shareable result card to the clipboard |
g |
Result trends from your history |
s |
Server selection |
t |
Theme selector |
c |
Edit configuration — theme, provider, engine values (←→ 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 | custom
= 1.0
= true # desktop notification on test complete (Unix)
[]
= 10 # length of each transfer phase
= 4 # parallel connections
= 10
[]
= "gradient" # "none" (default solid bg) | "gradient"
# from = "#0d1117" # gradient top colour — omit to use theme background
# to = "#000000" # gradient bottom colour — omit to use 50%-darkened bg
[[]] # 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.6
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. - State is plain data mutated only by reducers; the renderer just draws it.
- Metrics are pure, deterministic calculators, each independently unit-tested.
Use as a library
The engine has no UI dependencies — embed real speed tests in your own Rust application:
let provider = create?;
let engine = new?;
let servers = engine.load_servers.await?;
// stream typed EngineEvents while the test runs
See examples/embed.rs (cargo run --example embed) and the API docs.
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) - Gradient wallpaper support (
[wallpaper]in config)