Open-source, ad-free internet speed test. Self-hostable, privacy-respecting, and lightweight.
Platforms: Web · Android · (iOS / Desktop — roadmap) License: GPL-3.0
- Download, upload, ping, and jitter measurement
- Multi-stream parallel testing for accuracy
- ISP and IP detection (via self-hosted server)
- Test history (localStorage / Room DB on Android)
- Light and dark themes
- Material You dynamic colors on Android 12+
- Share results as plain text
- Zero ads, zero tracking, zero telemetry
netspeed/
├── web/ Vanilla JS web app (GitHub Pages)
│ ├── index.html
│ ├── css/main.css
│ └── js/
│ ├── config.js Runtime configuration
│ ├── speedtest.js Measurement engine
│ ├── history.js localStorage history
│ └── app.js UI controller
├── server/ Node.js test server (self-hostable)
│ ├── server.js
│ ├── package.json
│ └── Dockerfile
└── android/ Native Android app (Java, Material You)
└── app/src/main/java/io/github/hexadecinull/netspeed/
├── engine/ SpeedTestEngine, SpeedTestResult
├── ui/ Activities, Fragments, custom GaugeView
└── data/ Room database, DAO
Ping / Jitter
Sends 20 HEAD requests to /ping sequentially (150 ms apart), measures RTT with performance.now() (web) or System.nanoTime() (Android). Trims 1 outlier from each end, reports median. Jitter is mean absolute deviation of consecutive samples.
Download
Opens N parallel HTTP streams fetching chunks from /download?size=25 (25 MB each). Reads response body in a streaming fashion, accumulating total bytes. Speed is computed as (totalBytes × 8) / elapsed_seconds / 1,000,000 Mbps. Runs for the configured duration (default 12 s).
Upload
Generates a seeded random byte buffer (16 MB per stream) and POSTs it to /upload repeatedly until the duration expires. Tracks bytes sent via XMLHttpRequest.upload.onprogress (web) or OutputStream.write byte counting (Android).
docker build -t netspeed-server ./server
docker run -d \
-p 8080:8080 \
-e CORS_ORIGIN=https://yourdomain.com \
--name netspeed \
netspeed-servercd server
node server.jsEnvironment variables:
| Variable | Default | Description |
|---|---|---|
PORT |
8080 | Listen port |
CORS_ORIGIN |
* |
Allowed CORS origin |
MAX_UPLOAD_MB |
500 | Max upload body size |
GARBAGE_SIZE_MB |
100 | Max single download response size |
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_request_buffering off;
client_max_body_size 0;
}The web app is a static site in /web/. It deploys automatically via GitHub Actions on every push to main.
Configure your server URL directly in the Settings panel (⚙) — it persists in localStorage.
Requires Android 8.0+ (API 26). Material You dynamic colors activate automatically on Android 12+ (API 31).
Build with Android Studio or:
cd android
./gradlew assembleReleasePermissions required: INTERNET
- iOS (Swift, SwiftUI)
- Desktop (Electron wrapping web app for quick release; Qt/C++ native later)
- Packet loss estimation
- Multiple server selection UI
- IPv4/IPv6 toggle
- Result sharing image card
PRs welcome. Please follow the existing code style: no inline comments, complete files only, no placeholders.
GNU General Public License v3.0 — see LICENSE.