What networkQuality Actually Measures
Most speed tests measure peak throughput in isolation. They open one or a few TCP connections, push data, and report the maximum rate. That number tells you almost nothing about how the connection behaves when your CI pipeline is uploading artifacts while you are on a video call.
`networkQuality` uses Apple's Adaptive Bitrate (ABR) algorithm to saturate the link with multiple parallel streams in both directions simultaneously, then measures latency during that saturation. The result is the RPM score. A connection with 950 Mbps download but an RPM of 60 will feel sluggish under real workloads. A connection with 200 Mbps download and an RPM of 4000 will feel snappy.
The tool talks to Apple's measurement infrastructure at `mensura.cdn-apple.com` by default. You can override this with a custom server if you control one, which matters when you are testing internal network segments or want to avoid routing traffic through Apple's CDN.
# Basic run - no arguments needed
networkQuality
# Verbose output - adds per-interval data
networkQuality -v
# JSON output for scripting
networkQuality -s -c
# Bind to a specific interface (e.g., en1 for Wi-Fi, en0 for Ethernet)
networkQuality -I en1
Reading the Default Output
Without flags, `networkQuality` prints a live progress display and then a summary. Here is real output from our test machine on a 1 Gbps symmetric fiber connection:
``` Upload capacity: 934.282 Mbps Download capacity: 941.657 Mbps Upload flows: 20 Download flows: 12 Responsiveness: High (2714 RPM) Base RTT: 8 ms ```
The flow counts tell you how many parallel TCP streams the tool needed to saturate the link. A higher flow count to reach saturation can indicate bufferbloat or connection overhead issues even if the final throughput number looks good.
RPM categories map as follows: Low is below 600, Medium is 600-3999, High is 4000 and above. Our 2714 RPM result sits in Medium territory despite the near-gigabit throughput, which matched our observation that Zoom calls degraded slightly during large `rsync` transfers.
Base RTT is the minimum round-trip time measured during the test - essentially the latency floor before load effects are factored in. Compare this to your `ping` baseline to quantify how much bufferbloat your router or ISP is adding under load.
# Compare base RTT to unloaded ping
ping -c 20 mensura.cdn-apple.com
# Then run networkQuality and compare Base RTT
networkQuality
JSON Output and Scripting
The `-c` flag outputs a JSON summary to stdout after the test completes. Combine it with `-s` to suppress the interactive progress display. This combination is what you want for cron jobs, monitoring scripts, or log aggregation pipelines.
The JSON structure contains `dl_throughput`, `ul_throughput`, `responsiveness`, `base_rtt`, `dl_flows`, and `ul_flows` as top-level keys. All throughput values are in bits per second, not megabits, so divide by 1,000,000 for Mbps or 1,048,576 for Mibps depending on your reporting standard.
On our test server we pipe this into a lightweight shell function that writes to a CSV file for weekly trend analysis. If you are running a more formal observability stack, the JSON output drops cleanly into Telegraf's exec input plugin, which then feeds InfluxDB or whatever TSDB you are using.
#!/bin/bash
# Run networkQuality and log results to CSV
# Requires macOS Monterey 12.0 or later
OUTPUT=$(networkQuality -s -c 2>/dev/null)
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Parse with python3 (ships with macOS)
DL=$(echo "$OUTPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(round(d['dl_throughput']/1e6,2))")
UL=$(echo "$OUTPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(round(d['ul_throughput']/1e6,2))")
RPM=$(echo "$OUTPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['responsiveness'])")
RTT=$(echo "$OUTPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['base_rtt'])")
echo "$TIMESTAMP,$DL,$UL,$RPM,$RTT" >> /var/log/networkquality.csv
Running a Custom Measurement Server
Apple published the server-side specification for the measurement protocol under RFC 9298 (HTTP/3 MASQUE). The reference implementation is available at `github.com/network-quality/server` and is written in Go. Building it requires Go 1.18 or later.
Running your own server matters in three scenarios: you want to test internal network segments without hitting the public internet, you are in an environment where `mensura.cdn-apple.com` is blocked or filtered, or you need measurements that stay within a compliance boundary.
After building the server binary, point `networkQuality` at it with the `-C` flag followed by a URL to a JSON configuration file. The config file tells the client where to find the upload and download endpoints.
One practical use: we deployed the measurement server on a Mac Studio inside a corporate LAN to baseline internal switch performance between VLANs. The server needs HTTPS with a valid certificate - the client rejects self-signed certs by default. Use a cert from an internal CA trusted by the macOS keychain, or use Let's Encrypt if the server has a public FQDN.
# Build the reference server (requires Go 1.18+)
git clone https://github.com/network-quality/server
cd server
go build -o networkqualityd ./cmd/networkqualityd
# Run it (point to your cert and key)
./networkqualityd \
--listen-addr 0.0.0.0:4443 \
--cert /etc/ssl/certs/internal.crt \
--key /etc/ssl/private/internal.key
# Client side: point to your server config URL
networkQuality -C https://your-internal-server:4443/config
Comparing networkQuality to iperf3 and speedtest-cli
`iperf3` is the right tool when you own both endpoints and need raw TCP or UDP throughput numbers for capacity planning. It runs client-server pairs, gives you per-second interval data, and lets you tune window sizes, parallel streams, and test duration. Nothing beats it for benchmarking a new 10GbE switch or a WireGuard tunnel between two machines you control.
`speedtest-cli` hits Ookla's server network and reports download, upload, and ping. It is useful for a quick sanity check against an ISP SLA, and the `--json` flag makes scripting straightforward. The limitation is that it does not measure responsiveness under load - it measures latency before the throughput test starts.
`networkQuality` fills the gap between those two tools. You do not need a second machine or a server account. It runs unidirectional and bidirectional tests simultaneously, which reflects real workload conditions. In our experience, systems with identical `speedtest-cli` download scores can have 10x differences in RPM, and the RPM difference predicts perceived performance far better than raw throughput.
For a production Mac fleet running automated network checks, the three tools serve different purposes and are not substitutes for each other. Run `networkQuality` for daily responsiveness trending, `speedtest-cli` for ISP SLA documentation, and `iperf3` for point-to-point infrastructure validation.
# iperf3: test between two machines you control
# On the server
iperf3 -s
# On the client (10 parallel streams, 30 second test, reverse direction)
iperf3 -c 192.168.1.100 -P 10 -t 30 -R
# speedtest-cli: ISP SLA check with JSON output
speedtest-cli --json --secure | python3 -m json.tool
# networkQuality: responsiveness under load
networkQuality -s -c | python3 -m json.tool
Automating Tests with launchd
`cron` works on macOS but launchd is the native scheduler and handles sleep/wake events correctly, which matters if you are testing on a laptop that spends time in clamshell mode. A launchd agent running the logging script from the earlier section will survive reboots and resume correctly after sleep.
Place your plist in `~/Library/LaunchAgents/` for per-user scheduling, or in `/Library/LaunchDaemons/` if the test needs to run as root or at system level. The `StartInterval` key takes seconds - 3600 for hourly, 86400 for daily.
One detail that catches people: `networkQuality` requires an active network connection and will exit with an error if run too early during boot, before the network stack is ready. Add a `NetworkState` key to your plist or add a short `sleep 10` at the start of your wrapper script.
If you are managing a larger fleet and want to centralize these checks, the JSON output from the script integrates naturally with tools like Datadog's custom metrics API or a self-hosted Prometheus pushgateway. Teams doing heavier DevOps automation sometimes route these metrics through workflow platforms - taskbotshub.ai handles this kind of scheduled metric collection and alerting if you want to avoid building the glue code yourself.
Label
org.myunix.networkquality
ProgramArguments
/bin/bash
/usr/local/bin/nq-log.sh
StartInterval
3600
RunAtLoad
StandardErrorPath
/var/log/networkquality-error.log
Testing Specific Interfaces and Multi-Homed Macs
Mac Pros and Mac Studios in studio or server rack environments often have multiple active interfaces - Ethernet, Wi-Fi, and sometimes a Thunderbolt bridge or a USB-C Ethernet adapter. The `-I` flag lets you bind `networkQuality` to a specific interface by name.
Get your interface names with `ifconfig` or `networksetup -listallhardwareports`. On Apple Silicon Macs, `en0` is typically the first Ethernet port and `en1` is Wi-Fi, but this varies by hardware. Verify before scripting.
When testing a Mac with both a 2.5GbE Ethernet adapter and built-in Wi-Fi, we ran parallel tests on both interfaces to compare real-world performance. The 2.5GbE adapter hit 934 Mbps download with 3100 RPM. The Wi-Fi 6E radio on the same machine hit 780 Mbps download but only 1400 RPM on the same fiber connection - useful data for deciding which interface to prioritize for latency-sensitive workloads like remote desktop or SSH over a VPN.
# List all active interfaces
ifconfig | grep '^[a-z]' | awk -F: '{print $1}'
# More readable: use networksetup
networksetup -listallhardwareports
# Test Ethernet specifically
networkQuality -I en0 -s -c
# Test Wi-Fi specifically
networkQuality -I en1 -s -c
# Run both in parallel and compare (background the first)
networkQuality -I en0 -s -c > /tmp/nq-en0.json &
networkQuality -I en1 -s -c > /tmp/nq-en1.json &
wait
python3 -c "
import json
for iface, path in [('en0', '/tmp/nq-en0.json'), ('en1', '/tmp/nq-en1.json')]:
d = json.load(open(path))
print(f"{iface}: {d['dl_throughput']/1e6:.1f} Mbps down, {d['responsiveness']} RPM")
"
Interpreting RPM in the Context of Real Workloads
RPM translates directly to user experience in a way that raw throughput does not. Apple's published guidance maps RPM to real scenarios: 4000+ RPM supports video conferencing, cloud backups, and interactive SSH simultaneously without degradation. 2000-4000 RPM handles most workloads but shows some jitter under combined load. Below 1000 RPM and you will notice lag in interactive sessions when background transfers are running.
For sysadmins managing remote workers or distributed teams, RPM is a better diagnostic starting point than download speed when someone complains that their VPN feels slow while syncing files. A low RPM score points toward bufferbloat or queue management problems at the router or ISP level. The fix is usually enabling CAKE or FQ-CoDel on the router, not upgrading the ISP plan.
To confirm bufferbloat as the cause of a low RPM score, compare the `base_rtt` from `networkQuality -c` output against an unloaded `ping` to the same server. If `ping` shows 8ms but `base_rtt` under load is 80ms, that is a 72ms increase caused by queue buildup - classic bufferbloat. If they match, the latency floor is just geographic distance to the server.
Bufferbloat is router firmware territory on most home setups, but in a corporate environment it could be a misconfigured QoS policy on a managed switch or firewall. Having weekly RPM logs from a Mac in the office makes it trivial to correlate a degradation event with a firmware update or config change.
# Measure unloaded RTT to Apple's measurement server
ping -c 10 mensura.cdn-apple.com | tail -2
# Run networkQuality and extract base_rtt
networkQuality -s -c | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(f"Base RTT under load: {d['base_rtt']} ms")
print(f"RPM: {d['responsiveness']}")
print(f"DL: {d['dl_throughput']/1e6:.1f} Mbps")
print(f"UL: {d['ul_throughput']/1e6:.1f} Mbps")
"
Version and Availability Notes
`networkQuality` ships with macOS Monterey 12.0 and all later releases. It is located at `/usr/bin/networkQuality` - note the capital Q. The binary is not available on macOS Big Sur 11 or earlier. It also does not exist on iOS, iPadOS, or tvOS, though Apple's Network diagnostics framework in those OSes uses the same underlying measurement protocol.
There is no Homebrew formula or MacPorts port for this tool because it uses private Apple frameworks for the measurement infrastructure. You cannot run it on Linux. If you need Linux-equivalent responsiveness measurement, look at `flent` with the `rrul` test, which gives similar bidirectional saturation testing and latency-under-load data using `netperf` as the backend.
As of macOS Sequoia 15 (the current release as of mid-2026), the JSON schema for `-c` output has not changed from the Monterey version, so scripts written against the Monterey output still parse correctly. Apple has not published a deprecation notice for any fields. The binary location and flag syntax are stable across Monterey, Ventura, Sonoma, and Sequoia.
If you are scripting fleet diagnostics and need to handle machines that might be running Big Sur or earlier, add a version check at the top of your wrapper script.
# Check macOS version before calling networkQuality
OS_MAJOR=$(sw_vers -productVersion | cut -d. -f1)
if [ "$OS_MAJOR" -lt 12 ]; then
echo "networkQuality requires macOS 12.0 (Monterey) or later. Found: $(sw_vers -productVersion)"
exit 1
fi
# Confirm binary location
ls -la /usr/bin/networkQuality
# Get current macOS and tool info
sw_vers
man networkQuality | head -20