A modern iperf3 alternative with a live TUI, multi-client server, MPTCP, and QUIC support. Built in Rust.
# Server
xfr serve
# Client (in another terminal or machine)
xfr 192.168.1.1 # Basic TCP test
xfr 192.168.1.1 -b 100M # TCP at 100 Mbps
xfr 192.168.1.1 -P 4 # 4 parallel streams
xfr 192.168.1.1 -u -b 1G # UDP at 1 GbpsSee Installation below for setup instructions.
- Live TUI with real-time throughput graphs and per-stream stats
- Server dashboard -
xfr serve --tuifor monitoring active tests - Multi-client server - handle multiple simultaneous tests
- TCP, UDP, QUIC, and MPTCP with configurable bitrate pacing and parallel streams
- Firewall-friendly - single-port TCP and UDP (all traffic on 5201), QUIC multiplexing, and
--cportfor pinning data source ports - Bidirectional testing - measure upload and download simultaneously
- Multiple output formats - plain text, JSON, JSON streaming, CSV
- Result comparison -
xfr diffto detect performance regressions - LAN discovery - find xfr servers with mDNS (
xfr discover) - Prometheus metrics - export stats for monitoring dashboards
- Config file - save defaults in the platform configuration directory
- Environment variables -
XFR_PORT,XFR_DURATIONoverrides
| Feature | iperf3 | xfr |
|---|---|---|
| Live TUI | No | Yes (client & server) |
| Multi-client server | No | Yes |
| MPTCP | Yes (3.19+, Linux) | Yes (auto on server, --mptcp on client, Linux 5.6+) |
| Firewall-friendly | --cport (TCP/UDP) |
Single-port TCP & UDP + --cport |
| Output formats | Text/JSON | Text/JSON/CSV |
| Prometheus metrics | No | Yes (optional feature) |
| Compare runs | No | xfr diff |
| LAN discovery | No | xfr discover |
| Config file | No | Yes |
Measure actual throughput through your VPN:
# On VPN server
xfr serve
# From client, through VPN
xfr 10.8.0.1 -t 30sTest UDP at your expected rate to detect packet loss:
xfr <host> -u -b 500M -t 60s # Watch for loss percentage in TUIFind the largest UDP payload that survives the path — per direction, with the don't-fragment flag set so middleboxes can't hide the limit by fragmenting (useful for NFS-over-UDP and VPN/tunnel tuning):
xfr <host> --probe-mtuQuantify the impact of network changes:
xfr <host> --json -o before.json
# ... make changes ...
xfr <host> --json -o after.json
xfr diff before.json after.json --threshold 5Test aggregate bandwidth across bonded/LACP interfaces:
xfr <host> -P 8 -t 30s # 8 streams to utilize all linksContinuous performance monitoring:
xfr serve --prometheus 9090 --push-gateway http://pushgateway:9091
# Scrape metrics or view in GrafanaRequires Rust 1.88+:
# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# Install xfr
cargo install xfrbrew install lance0/tap/xfrMulti-arch image (amd64/arm64), handy for running a server:
docker run --rm -p 5201:5201 -p 5201:5201/udp ghcr.io/lance0/xfr:latest serveDownload from GitHub Releases:
| Platform | Target |
|---|---|
| Linux x86_64 | xfr-x86_64-unknown-linux-musl.tar.gz |
| Linux ARM64 | xfr-aarch64-unknown-linux-gnu.tar.gz (or -musl for fully static) |
| macOS Apple Silicon | xfr-aarch64-apple-darwin.tar.gz |
| macOS Intel | Use cargo install xfr |
| Android (Termux) | xfr-aarch64-linux-android.tar.gz |
| Windows | Use WSL2 (native support is experimental) |
# Example: Linux x86_64
curl -LO https://github.com/lance0/xfr/releases/latest/download/xfr-x86_64-unknown-linux-musl.tar.gz
tar xzf xfr-*.tar.gz && sudo mv xfr /usr/local/bin/eget lance0/xfryay -S xfr-bingit clone https://github.com/lance0/xfr
cd xfr && cargo build --release
sudo cp target/release/xfr /usr/local/bin/Note: Review scripts before piping to sh. See the install script source.
curl -fsSL https://raw.githubusercontent.com/lance0/xfr/master/install.sh | shDownload the aarch64-linux-android binary from releases, or build from source:
pkg install rust
cargo install xfrnix run github:lance0/xfr # Run without installing
nix profile install github:lance0/xfr # Install to profileOr add to your flake inputs:
inputs.xfr.url = "github:lance0/xfr";A dev shell is also available via nix develop.
Available via pkgsrc:
pkgin install xfr| Feature | Default | Description |
|---|---|---|
discovery |
Yes | mDNS LAN discovery (xfr discover) |
prometheus |
No | Prometheus metrics endpoint and Push Gateway support |
cargo install xfr --features prometheus # Prometheus support
cargo install xfr --all-features # All features# Bash
xfr --completions bash > ~/.local/share/bash-completion/completions/xfr
# Zsh (add ~/.zfunc to fpath in .zshrc first)
xfr --completions zsh > ~/.zfunc/_xfr
# Fish
xfr --completions fish > ~/.config/fish/completions/xfr.fish
# PowerShell (add to $PROFILE)
xfr --completions powershell >> $PROFILE
# Elvish
xfr --completions elvish > ~/.elvish/lib/xfr.elvxfr serve # Listen on port 5201
xfr serve -p 9000 # Custom port
xfr serve --tui # Live dashboard showing active tests
xfr serve --one-off # Exit after one test
xfr serve --max-duration 60s # Limit test duration
xfr serve --push-gateway http://pushgateway:9091 # Push metrics on test complete
xfr serve --psk mysecret # Require PSK authentication
xfr serve --rate-limit 2 # Max 2 concurrent tests per IP
xfr serve --allow 192.168.0.0/16 # Only allow the local subnetxfr 192.168.1.1 # TCP test, 10s, single stream
xfr 192.168.1.1 -t 30s # 30 second test
xfr 192.168.1.1 -P 4 # 4 parallel streams
xfr 192.168.1.1 -R # Reverse (download test)
xfr 192.168.1.1 --bidir # Bidirectional
xfr 192.168.1.1 -6 # Force IPv6 only
xfr ::1 -6 # IPv6 localhostxfr 192.168.1.1 -u # UDP mode
xfr 192.168.1.1 -u -b 1G # UDP at 1 Gbps
xfr 192.168.1.1 -u -b 100M # UDP at 100 Mbpsxfr 192.168.1.1 --quic # QUIC transport (encrypted)
xfr 192.168.1.1 --quic -P 4 # QUIC with 4 parallel streams
xfr 192.168.1.1 --quic -R # QUIC download testQUIC provides built-in TLS 1.3 encryption with stream multiplexing over a single connection.
Security Note: QUIC encrypts each connection but does not verify server
identity without a PSK. With a PSK, xfr binds mutual authentication and the
protected control channel to the exact TLS connection using the RFC 9266
exporter, so a terminating relay cannot splice the handshake onto another QUIC
connection. Both peers must support quic_channel_binding_v1; QUIC + PSK fails
closed otherwise. The self-signed certificate is still not a public PKI identity.
xfr 192.168.1.1 --mptcp # MPTCP (Multi-Path TCP, Linux 5.6+)
xfr 192.168.1.1 --mptcp -P 4 # MPTCP with 4 parallel streams
xfr 192.168.1.1 --mptcp -R # MPTCP download testMPTCP enables a single connection to use multiple network paths simultaneously (e.g., WiFi + Ethernet). The server automatically creates MPTCP listeners — no flag needed on the server side. All TCP features (nodelay, congestion control, window size, bidir, multi-stream) work transparently with MPTCP.
xfr <host> --json # JSON summary
xfr <host> --json-stream # JSON per interval (for scripting)
xfr <host> --csv # CSV output
xfr <host> -q # Quiet mode (summary only)
xfr <host> -o results.json # Save to file
xfr <host> --no-tui # Plain text, no TUI
xfr <host> --timestamp-format iso8601 # ISO 8601 timestampsNote: Log messages go to stderr, allowing clean JSON/CSV piping: xfr <host> --json 2>/dev/null
xfr <host> -i 2 # Report every 2 seconds
xfr <host> --omit 3 # Skip first 3s of intervals (TCP ramp-up)xfr diff baseline.json current.json
xfr diff baseline.json current.json --threshold 5xfr discover # Find xfr servers on LAN
xfr discover --timeout 10s # Extended search| Key | Action |
|---|---|
q |
Quit (cancels test) |
p |
Pause/Resume test traffic |
s |
Settings modal |
t |
Cycle color theme |
d |
Toggle per-stream view |
? / F1 |
Help |
j |
Print JSON result |
u |
Dismiss update notification |
| Key | Action |
|---|---|
q |
Quit server |
? / F1 |
Help |
Esc |
Close help |
xfr includes 11 built-in color themes. Select with --theme or press t during a test:
xfr <host> --theme dracula # Dark purple theme
xfr <host> --theme matrix # Green on black hacker style
xfr <host> --theme catppuccin # Soothing pastels
xfr <host> --theme nord # Arctic blue tonesAvailable themes: default, kawaii, cyber, dracula, monochrome, matrix, nord, gruvbox, catppuccin, tokyo_night, solarized
The monochrome theme (alias mono) is the high-visibility choice: it uses
the terminal's own default colors at maximum contrast, so it works on both
dark and light terminal backgrounds — pair it with a light background at full
brightness when working in direct sunlight. Loss severity in the throughput
graph is always also encoded without color (light loss underlines the bar,
heavy loss reverses the cell), so lossy intervals stay visible through glare,
in monochrome, and for colorblind users. Setting the standard
NO_COLOR environment variable selects monochrome
automatically unless a theme is chosen explicitly.
Your theme preference is auto-saved beside config.toml as xfr/prefs.toml
in the same platform configuration directory.
xfr reads defaults from:
- Linux:
$XDG_CONFIG_HOME/xfr/config.toml, or~/.config/xfr/config.tomlwhenXDG_CONFIG_HOMEis unset - macOS:
~/Library/Application Support/xfr/config.toml - Windows:
%APPDATA%\xfr\config.toml
[client]
duration_secs = 10
parallel_streams = 1
tcp_nodelay = false
window_size = "1M" # TCP/UDP socket buffer size (SO_SNDBUF/SO_RCVBUF), e.g. "512K", "2M"
# bitrate = "100M" # target bitrate; accepts the same units as --bitrate
# congestion = "bbr" # TCP congestion-control algorithm
# dscp = "EF" # DSCP name or raw TOS byte (0-255)
interval_secs = 1.0 # report interval; default is 1.0 seconds
json_output = false
no_tui = false
theme = "default" # or dracula, catppuccin, nord, matrix, etc.
timestamp_format = "relative" # or "iso8601", "unix"
address_family = "dual" # "ipv4", "ipv6", or "dual"
omit_secs = 0 # omit first N seconds (TCP ramp-up)
# bind = "192.168.1.100" # local address, optionally with :port
# cport = 5202 # client source port for firewall traversal
psk = "my-secret-key"
log_file = "~/.config/xfr/xfr.log"
log_level = "info"
[server]
port = 5201
one_off = false
no_mdns = false
address_family = "dual" # "ipv4", "ipv6", or "dual"
psk = "my-secret-key"
rate_limit = 5
rate_limit_window = 60
allow = ["192.168.0.0/16", "10.0.0.0/8"]
deny = []
acl_file = "/path/to/acl.txt"
push_gateway = "http://pushgateway:9091"
log_file = "~/.config/xfr/xfr-server.log"
log_level = "info"On Unix, a config containing either psk must grant no access to group or
other users, so the shared key is not exposed to another local account:
# Linux
chmod 600 "${XDG_CONFIG_HOME:-$HOME/.config}/xfr/config.toml"
# macOS
chmod 600 "$HOME/Library/Application Support/xfr/config.toml"The value-taking transport and output settings under [client] are defaults: an explicit CLI value takes precedence. This includes omit_secs and interval_secs, so --omit 0 and --interval 1.0 override the config file. bitrate accepts the same 100M/1G syntax as --bitrate; dscp accepts the same DSCP names and numeric values as --dscp.
Environment variables override config file:
export XFR_PORT=9000
export XFR_DURATION=30sEnable with --features prometheus:
xfr serve --prometheus 9090Metrics available at http://localhost:9090/metrics include:
xfr_bytes_total,xfr_throughput_mbps,xfr_tests_total, andxfr_active_tests- aggregate transfer and server statexfr_test_duration_seconds- test duration histogramxfr_stream_bytes_total,xfr_stream_throughput_mbps, andxfr_stream_retransmits_total- per-stream metrics labeled bytest_idandstream_idxfr_tcp_rtt_microsecondsandxfr_tcp_retransmits_total- TCP metrics labeled bytest_id
See examples/grafana-dashboard.json for a sample Grafana dashboard.
| Flag | Short | Default | Description |
|---|---|---|---|
--port |
-p |
5201 | Server/client port |
--time |
-t |
10s | Test duration (use 0 for infinite) |
--bytes |
-n |
none | Transfer a fixed number of bytes instead of running for a fixed time (e.g. 1G; K/M/G are binary). Total across all streams; -t becomes an upper bound if also given |
--udp |
-u |
false | UDP mode |
--quic |
-Q |
false | QUIC mode (encrypted, multiplexed streams) |
--bitrate |
-b |
unlimited | Target bitrate for TCP and UDP (e.g., 1G, 100M). 0 = unlimited. Global across all streams |
--parallel |
-P |
1 | Parallel streams |
--reverse |
-R |
false | Reverse direction (download) |
--bidir |
false | Bidirectional test | |
--ipv4 |
-4 |
false | Force IPv4 only |
--ipv6 |
-6 |
false | Force IPv6 only |
--bind |
none | Local address to bind (e.g., 192.168.1.100) | |
--cport |
none | Client source port for firewall traversal (UDP/QUIC/TCP data streams) | |
--dscp |
none | DSCP/TOS marking for TCP/UDP QoS testing (0-255 or name: EF, AF11, CS1, etc.) | |
--mptcp |
false | MPTCP mode (client-only, Linux 5.6+; server auto-enables) | |
--random |
true | Use random payload data for client-sent TCP/UDP traffic (default) | |
--zeros |
false | Use zero-filled payload data (client-sent traffic only) | |
--zerocopy |
-Z |
true (TCP) | Zero-copy TCP sends via sendfile(2), like iperf3 -Z (Linux; lowers sender CPU overhead). On by default; explicit -Z warns when zero-copy can't take effect |
--no-zerocopy |
false | Disable zero-copy TCP sends (use regular buffered writes) | |
--probe-mtu |
false | Probe the path MTU per direction instead of running a throughput test (UDP + DF bit; needs server ≥ this version) | |
--connect-timeout |
none | Fail if the control connection takes longer than this (e.g. 5s); without it, dead servers are bounded only by OS defaults |
|
--json |
false | JSON output | |
--json-stream |
false | JSON per interval | |
--csv |
false | CSV output | |
--quiet |
-q |
false | Summary only |
--interval |
-i |
1.0 | Report interval (seconds) |
--omit |
0 | Omit first N seconds | |
--output |
-o |
stdout | Output file |
--no-tui |
false | Disable TUI | |
--no-update-check |
false | Disable the background update check (also honors DO_NOT_TRACK / XFR_NO_UPDATE_CHECK) |
|
--theme |
default | Color theme (dracula, nord, matrix, etc.) | |
--tcp-nodelay |
false | Disable Nagle algorithm | |
--window |
-w |
OS default | Socket buffer size for TCP and UDP (SO_SNDBUF/SO_RCVBUF on both ends); when unset, TCP autotunes and UDP uses the kernel default |
--congestion |
OS default | TCP congestion control algorithm (e.g. cubic, bbr, reno) | |
--timestamp-format |
relative | Timestamp format (relative, iso8601, unix) | |
--log-file |
none | Log file path (e.g., ~/.config/xfr/xfr.log) | |
--log-level |
info | Log level (error, warn, info, debug, trace) | |
--push-gateway |
none | Prometheus Push Gateway URL (server) | |
--prometheus |
none | Prometheus metrics port (server, requires feature) | |
--psk |
none | Pre-shared key for authentication | |
--psk-file |
none | Read PSK from file | |
--rate-limit |
none | Max concurrent tests per IP (server) | |
--rate-limit-window |
60s | Rate limit time window (server) | |
--completions |
none | Generate shell completions (bash, zsh, fish, powershell, elvish) | |
--allow |
none | Allow IP/subnet, repeatable (server) | |
--deny |
none | Deny IP/subnet, repeatable (server) | |
--acl-file |
none | ACL rules file (server) | |
--max-duration |
none | Maximum test duration, server-side limit (server) | |
--tui |
false | Enable live dashboard (server) | |
--one-off |
false | Exit after one test (server, works with TCP and QUIC) | |
--no-mdns |
false | Disable mDNS service registration (server) |
TCP and UDP tests use random payloads by default to avoid inflated results on WAN-optimized or compressing paths. --random and --zeros control client-sent traffic. Server-sent TCP/UDP traffic also defaults to random, but payload mode is not negotiated over the wire.
--dscp applies to TCP and UDP sockets on both ends when relevant, including server-sent download and bidirectional traffic. QUIC ignores it because the underlying socket is managed by Quinn, and non-Unix platforms currently warn instead of applying socket marking.
| Mode | Bulk test data | Control channel |
|---|---|---|
| TCP | Plaintext | Plaintext without PSK; ChaCha20-Poly1305 after PSK authentication |
| UDP | Plaintext | Uses the same TCP control channel behavior |
| QUIC | TLS 1.3; identity unverified without PSK | TLS 1.3; exporter-bound PSK protection when enabled |
QUIC mode (-Q/--quic) uses a self-signed server certificate without PKI
verification. Without a PSK, an active relay can terminate and forward the QUIC
connection. With a PSK, xfr mixes the RFC 9266 TLS exporter into the
authentication proof and protected-control keys, binding them to the exact QUIC
connection that carries the bulk streams. A relay cannot splice authentication
across two QUIC connections without the PSK. This requires both peers to
advertise quic_channel_binding_v1; the session fails closed otherwise.
PSK authentication (--psk) authenticates both peers and protects post-auth
control messages with ChaCha20-Poly1305. TCP and UDP bulk test payloads remain
plaintext. QUIC bulk payloads are TLS-encrypted, and on upgraded PSK sessions
that TLS connection is authenticated by the exporter-bound PSK handshake. The
self-signed certificate is still not a public PKI identity; use an authenticated
VPN when you need centrally managed endpoint identities or isolation between
different holders of a shared PSK.
To combine QUIC transport encryption with PSK-protected control:
# Server
xfr serve --psk "secretkey"
# Client (QUIC transport + PSK-protected control)
xfr <host> -Q --psk "secretkey"Security note:
--pskand theXFR_PSKenvironment variable expose the key through process metadata such asps, shell history, and/proc/<pid>/environ. For production deployments, store the key in a file readable only by the owner and use--psk-fileinstead.
- Single-port TCP: TCP uses single-port mode by default -- control and data connections share port 5201. Data connections are validated against the control connection's IP address, preventing unauthorized access.
- UDP on untrusted networks: UDP mode may be susceptible to reflection attacks from spoofed source addresses. Use TCP or QUIC on public networks.
- Rate limiting: Use
--rate-limiton public servers to prevent abuse. - ACLs: Use
--allow/--denyto restrict client access.
- Slow-loris resistance: New connections must send their first message within 5 seconds, preventing slow-loris attacks from blocking the accept loop.
- DataHello flood protection: DataHello messages for unknown test IDs are rejected immediately without allocating resources.
- Bounded reads: All control messages are limited to 8KB, preventing memory exhaustion from oversized messages.
- Capability negotiation: Client and server exchange capabilities during the Hello handshake (protocol version 1.1), enabling safe feature evolution.
- Concurrent connection limits: Server limits concurrent handlers (default 100) to prevent connection floods.
Each TCP stream allocates a 128 KB application read/write buffer. Kernel socket buffers are managed by TCP autotuning unless the client passes -w/--window, in which case the requested size is applied via SO_SNDBUF/SO_RCVBUF on both ends. Memory usage scales with concurrent clients:
| Streams per client | App buffer per client | 10 clients (app buffers) |
|---|---|---|
1 (-P 1) |
128 KB | 1.3 MB |
8 (-P 8) |
1 MB | 10 MB |
128 (-P 128) |
16 MB | 160 MB |
On top of that, the kernel holds autotuned socket buffers (typically a few hundred KB per stream, capped by net.ipv4.tcp_rmem[2]/tcp_wmem[2]). When a client passes -w N, add roughly N bytes per stream on each side. The server limits concurrent handlers (default 100) to prevent resource exhaustion. Use --rate-limit to restrict tests per IP.
| Platform | Status |
|---|---|
| Linux x86_64/ARM64 | Full support, pre-built binaries |
| macOS Apple Silicon | Full support, pre-built binaries |
| macOS Intel | Full support, build from crate: cargo install xfr |
| Android (Termux) | Full support, pre-built binaries |
| NetBSD | Full support, via pkgsrc: pkgin install xfr |
| Windows | Experimental (WSL2 recommended). Native builds work but lack TCP_INFO metrics. |
Use a port above 1024 or run with elevated privileges:
xfr serve -p 9000Ensure the server is running and the port is not blocked by a firewall. TCP, UDP, and QUIC all run entirely on port 5201 (or your custom port) when both ends are current -- no server-side ephemeral data ports are needed (UDP single-port requires both ends ≥ v0.9.18; older pairings fall back to ephemeral server ports automatically). For strict egress policies or ECMP testing, use --cport to pin client source ports.
- Try multiple parallel streams:
-P 4 - Disable Nagle's algorithm:
--tcp-nodelay - Increase TCP socket buffer:
--window 4M - On CPU-bound senders (embedded routers, SBCs), the per-write userspace copy is skipped by default via sendfile(2) (TCP, Linux). This applies to client sends, and to server sends in
-R/--bidirwhen the server supports it; pass-Zto get a warning when zero-copy can't take effect, or--no-zerocopyto disable it
- Reduce bitrate:
-b 500M - Check for network congestion or firewall issues
- If the receiver may be CPU-bound or its kernel UDP buffer is small, increase
--window(e.g.-w 16M) —-wapplies to UDPSO_SNDBUF/SO_RCVBUFand propagates to the server, helping high-rate flows avoid kernel tail-drops that hide as live0.0%loss
- Comparison with iperf3 - Feature matrix and migration guide
- Scripting & CI/CD - Automation, Docker, Prometheus
- Features Reference - Detailed feature documentation
- Architecture - For contributors
- Changelog - Release history
- Known Issues - Edge cases and limitations
- Roadmap - Planned features
- Contributing - Development guidelines
- Security Policy - Supported versions and private reporting
- Terminal Trove - xfr listing and discovery
- AUR - Arch Linux package (community-maintained)
- pkgsrc - NetBSD package (community-maintained)
Special thanks to Matthieu Baerts (matttbe), Linux kernel MPTCP co-maintainer, for extensive testing, detailed bug reports with packet traces, and feature suggestions including MPTCP support, kernel TCP pacing, zero-copy IO, and high stream-count hardening. xfr is significantly better because of his contributions.
Licensed under either of Apache License, Version 2.0 or MIT license at your option.

