- Python 43.5%
- CSS 24.6%
- HTML 14.7%
- JavaScript 12%
- PowerShell 1.8%
- Other 3.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| .github/workflows | ||
| deploy | ||
| docs | ||
| scripts | ||
| src/speedprobe | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| compose.prod.yml | ||
| compose.yml | ||
| Dockerfile | ||
| LICENSE | ||
| Makefile | ||
| pyproject.toml | ||
| README.md | ||
| speedtest.py | ||
| uv.lock | ||
Speedprobe
One URL. Ten sequential downloads. Clear timing, byte counts, and throughput.
A Python CLI that completes the original assignment on your computer, with a browser dashboard, HTTP API, WebSocket progress, and self-hosted 1 / 5 / 20 MB PNG fixtures.
Live demo · Source repository · Design contract · CI/CD · Forgejo Actions
Quick install
Linux / macOS:
curl -fsSL https://git.ligand.su/akadmin/speedprobe/raw/tag/v1.0.1/scripts/install.sh | sh
Windows PowerShell:
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; irm https://git.ligand.su/akadmin/speedprobe/raw/tag/v1.0.1/scripts/install.ps1 | iex
Then run:
speedprobe
Choose a 1, 5, or 20 MB image in the terminal. For scripts, use speedprobe --size 5 --json; pass any HTTP(S) URL to test your own target.
Installation uses your user profile. The installer downloads uv and Python 3.12 when needed. Installation details, script review, and removal.
Run from source
Requires Python 3.12+ and uv.
git clone https://git.ligand.su/akadmin/speedprobe.git
cd speedprobe
uv sync --frozen
uv run speedprobe
You can also use uv run python speedtest.py URL or uv run python -m speedprobe URL.
The CLI makes exactly ten sequential GET requests, fully downloads each response before starting the next, and prints the result. Example output (illustrative values):
1/10 5.000 MB 0.421 s 11.876 MB/s
...
Requests: 10; downloaded: 50,000,000 bytes
Mean request time: 0.425 s
Throughput: 11.765 MB/s (94.118 Mbps)
uv run speedprobe https://example.com/large-image.jpg --json
uv run speedprobe https://example.com/large-image.jpg --timeout 20 --max-mib 32
--json returns the summary and all ten samples. Errors also produce JSON and exit code 1; Ctrl+C exits with code 130. Redirects are rejected: supply the final URL. The CLI accepts any HTTP(S) target, rejects embedded credentials, and ignores proxy environment variables.
Measurement model
- Request duration: from the start of GET until the full response body arrives, including connection setup and server latency. The client reuses connections.
- Downloaded bytes: response body bytes received; HTTP/TLS headers and protocol overhead are excluded.
- Mean duration:
sum(elapsed) / 10. - Throughput:
sum(bytes) / sum(elapsed) / 1_000_000MB/s. This is aggregate throughput, rather than the arithmetic mean of individual request rates. - Browser display: Mbps is the primary speed reading; the method panel explains its conversion to MB/s. The CLI retains MB/s as required by the assignment. This changes the display unit, not measured bandwidth.
- Units: 1 MB = 1,000,000 bytes; 1 Mbps = 1,000,000 bits/second;
Mbps = MB/s × 8. The--max-miblimit uses binary MiB.
Results describe the route to the selected server, rather than guaranteed ISP capacity. Smaller files are more sensitive to latency. Python streams each response with bounded memory and rejects compression; browser downloads bypass the client cache and count decoded response bytes. Defaults are 30 seconds and 64 MiB per response. An interrupted or failed run never reports partial measurements as success.
Browser, HTTP, and WebSocket
Start the Python application:
uv run uvicorn speedprobe.api:app --host 127.0.0.1 --port 8000
Or start the complete local stack with Nginx and generated fixtures:
docker compose up --build -d
Open http://localhost:18120/. See deployment instructions.
Browser downloads measure your computer → fixture server. HTTP and WebSocket APIs run Python downloads along application server → fixture server. Their results demonstrate the server-side protocols and do not measure the visitor's internet connection.
curl https://ligand.su/speedtest/api/fixtures
curl -X POST https://ligand.su/speedtest/api/measure \
-H 'Content-Type: application/json' \
-d '{"url":"https://ligand.su/speedtest/fixtures/5mb.png","requests":10}'
Connect to wss://ligand.su/speedtest/ws/measure and send the same JSON payload as the first message. The server emits progress messages containing sample, completed, and total, followed by a summary result or an error message. Disconnecting cancels the download. Interactive OpenAPI documentation is available at /speedtest/docs.
The public API accepts only exact URLs of configured fixtures, rejects arbitrary targets, and never follows redirects, preventing visitor-controlled SSRF. Defaults allow two concurrent runs, four starts per minute per IP, and ten downloads per run. The application uses one worker; horizontal scaling requires shared admission and rate-limit state or enforcement at the edge.
Development
uv sync --frozen
uv run pytest
uv run ruff check .
| Path | Responsibility |
|---|---|
src/speedprobe/core.py |
Streaming measurement engine, independent of UI |
src/speedprobe/cli.py |
Terminal interface |
src/speedprobe/api.py |
FastAPI, fixture allowlist, admission limits, WebSocket |
src/speedprobe/static/ |
Browser dashboard |
tests/ |
Calculations, sequential downloads, errors, limits, and API behavior |
deploy/ |
Self-hosting and generated fixtures |
Application settings: SPEEDPROBE_PUBLIC_ORIGIN (origin without a path), SPEEDPROBE_ROOT_PATH (such as /speedtest), SPEEDPROBE_FIXTURE_ORIGIN (trusted internal Nginx origin), and SPEEDPROBE_CONCURRENCY (1–16). SPEEDPROBE_FIXTURES optionally supplies a JSON array of {id,label,bytes,url} objects; this is trusted administrator configuration. API downloads use the internal fixture origin selected at startup; the visitor's URL is only an allowlist lookup key.
The application runs as a non-root user. Container builds use the frozen uv.lock; Nginx serves the static test files from tmpfs with cached file descriptors. Deployment documentation explains caching and resource boundaries. Automated tests require no external network access. Release acceptance evidence records checks and platform limitations.
MIT licensed.