Measures download, upload, and latency with the official Ookla Speedtest CLI and falls back to LibreSpeed when Speedtest is blocked or unavailable.
Features
- Zero setup — Ookla Speedtest first, LibreSpeed when it is blocked or fails.
- Automatic binaries — downloaded and cached for your OS on first run.
- Nearby servers — the lowest-latency server is picked automatically.
- Self-recovery — failed attempts are retried, broken servers excluded.
- Uniform results — same fields and units whichever provider ran; client info from geo-IP.
pip install speedkitfrom speedkit import Speedkit
kit = Speedkit()
result = kit.run()
print(f"download: {result.download / 1_000_000:.1f} Mbit/s")
print(f"upload: {result.upload / 1_000_000:.1f} Mbit/s")
print(f"ping: {result.ping:.1f} ms")
data = result.to_dict() # plain JSON-serializable dictOr from the command line:
speedkit # auto: Ookla, then LibreSpeed
speedkit -p librespeed # pin one provider
speedkit -t 180 # time budget per provider in seconds
speedkit -a 1 # disable retries
speedkit --no-geoip # no third-party geo-IP requestSpeedkit() needs no configuration; everything it accepts:
kit = Speedkit(
provider="auto", # "auto" | "ookla" | "librespeed" — a pinned provider never falls back
timeout=120, # time budget per provider in seconds, covering all attempts
attempts=2, # measurement attempts within the budget; 1 disables retries
lookup_geoip=True, # resolve client info from a geo-IP service; False makes no such request
cache_dir=None, # where CLI binaries are cached; None = user cache directory
)SpeedtestResult.to_dict() returns download/upload in bits per second and ping in milliseconds:
{
"client": {
"ip": "203.0.113.7",
"isp": "Example ISP",
"country": "ZZ",
"city": "Springfield"
},
"server": {
"name": "Springfield",
"country": "Freedonia",
"sponsor": "Example Sponsor",
"id": "1234",
"host": "speedtest.example.net:8080",
"latency": 0.681
},
"provider": "ookla",
"download": 293111520.0,
"upload": 292265640.0,
"ping": 0.681,
"timestamp": "2026-07-19T15:22:48Z",
"bytes_sent": 418465064,
"bytes_received": 426981996
}client— from a geo-IP service, identical whichever provider measured.server— the test server that provider picked; unreported fields stay at""/0.0.provider— which measurer produced the result.
Client details come from the first service that answers: ipinfo.io → ipwho.is.
- The lookup takes a fraction of a second.
- If every service is unreachable, the measurement is still returned — with whatever client info the CLI reported.
This sends your IP address to the geo-IP service that answers.
Pass lookup_geoip=False (or --no-geoip) to skip it entirely. Nothing is then sent to a third
party, and client keeps what the measuring CLI itself reported — ip and isp from
Ookla, plus country from LibreSpeed. Fields neither reports, city above all, stay at
"", so client is no longer identical across providers.
| Provider | Version | Platforms |
|---|---|---|
| Ookla Speedtest CLI | 1.2.0 | Linux x86_64/aarch64, macOS |
| librespeed-cli | 1.0.13 | Linux x86_64/aarch64, macOS |
Binaries are downloaded on first use and cached per platform:
- Linux —
~/.cache/speedkit - macOS —
~/Library/Caches/speedkit
Every archive is checked against a SHA-256 digest pinned in speedkit/binaries.py before
anything is extracted or made executable. A release asset that changed after it was pinned
raises BinaryDownloadError instead of running. All network access is HTTPS with certificate
verification; plain HTTP, and any redirect leaving TLS, is refused.
Set SPEEDKIT_OOKLA_BINARY / SPEEDKIT_LIBRESPEED_BINARY to a path of a preinstalled
binary to skip downloading entirely — useful for offline machines and locked-down networks.
The path is executed as given and bypasses the checksum, so it is on you to trust it.
To pre-download the binaries at image build time (Docker, CI):
python -c "from speedkit.binaries import ookla_binary, librespeed_binary; ookla_binary(); librespeed_binary()"Running the Ookla provider passes
--accept-license --accept-gdpr, which implies acceptance of the Ookla EULA and privacy terms.
Every error derives from SpeedkitError and carries an actionable hint:
from speedkit import Speedkit, SpeedkitError
try:
result = Speedkit().run()
except SpeedkitError as error:
print(error) # cause, and a hint on how to fix itUnsupportedPlatformError — no prebuilt binary for this OS/arch; BinaryDownloadError —
the binary could not be fetched; SpeedtestError — the measurement itself failed.
This repository is distributed under the MIT License.