gospeedtest server reference¶
Run the HTTP server that hosts the web UI and the speed-test endpoints.
Flags¶
| Flag | Default | Purpose |
|---|---|---|
--addr |
:8080 |
Listen address. Use :0 for an ephemeral port, or 127.0.0.1:8080 to bind only to localhost. |
--ipinfo-token |
(none) | Optional ipinfo.io token — passed as ?token=… on lookup. Without one, gospeedtest falls back to ipinfo's free tier (subject to undocumented per-source-IP rate limits). With one, you get a higher quota and more reliable results. See Privacy. |
--trust-proxy-headers |
false |
When set, the server reads the client IP from X-Forwarded-For (first entry) or X-Real-IP instead of the TCP peer address. Off by default — see the warning below. |
Don't enable --trust-proxy-headers on a directly-exposed server
Without a sanitizing reverse proxy in front, any caller can send
X-Forwarded-For: <any.public.ip> and force the server to issue an
outbound ipinfo.io lookup for an IP of their choosing — draining
your ipinfo quota and turning /api/info into an SSRF-style oracle
that returns the lookup result. Only enable this flag when the
server sits behind an nginx / Caddy / Traefik / cloud-LB instance
that strips and re-sets these headers itself.
HTTP endpoints¶
The server registers the protocol endpoints (described in
How the test works) plus serves the embedded web UI
(index.html, style.css, app.js, etc.) from /.
/ws — WebSocket ping (browser-only)¶
The browser UI upgrades to a WebSocket on this path. The server then
drives a ping loop using WS Ping/Pong control frames — see
How the test works for why this is more accurate than
HTTP-based pinging from a browser. The CLI does not use this endpoint;
it pings via plain HTTP /ping because the Go HTTP client has no IPC
overhead.
The connection is bounded to 30 s of total lifetime so a misbehaving client cannot pin a server goroutine indefinitely.
/api/info returns:
{
"client_ip": "203.0.113.5",
"isp": "Acme Internet (AS64500)",
"city": "Berlin",
"region": "Berlin",
"country": "DE",
"server_host": "gst-01.example.com",
"server_time": "2026-05-01T11:11:40Z"
}
isp, city, region, and country are best-effort: the server
queries ipinfo.io with a 2-second timeout. On error
or timeout, those fields are omitted. Private and loopback IPs are
skipped — see Privacy.
Running behind a reverse proxy¶
By default the server reads the client IP from the TCP peer address
(r.RemoteAddr) — proxy headers are ignored so that a malicious
direct caller can't spoof their IP. To honor them, run with
--trust-proxy-headers. With the flag set, /api/info resolves
client_ip in this order:
- The first entry of
X-Forwarded-For, if present. - Otherwise,
X-Real-IP. - Otherwise, the host portion of
r.RemoteAddr.
So nginx / Caddy / Traefik reverse-proxy setups Just Work — as
long as your proxy strips and re-sets these headers itself.
The server intentionally has no WriteTimeout — long downloads
must not be cut off mid-stream — and a 60-second ReadTimeout to bound
slow uploads.
nginx snippet
Hardening posture and public-internet exposure¶
This binary intentionally serves bandwidth-heavy, unauthenticated endpoints with open CORS — that's what a speed-test server is. Read this section before exposing an instance to the public internet.
What's already in place:
ReadHeaderTimeout: 5s— bounds slowloris-style header-dribbling.ReadTimeout: 60s— bounds slow uploads.IdleTimeout: 60s— bounds idle keep-alive connections.MaxDownloadMiB/MaxUploadMiB— per-request byte caps (see below). Each individual request can't stream forever, but the server has no global concurrency cap and no per-IP rate limit.
What this binary does not do, and why it matters on the open internet:
- No authentication. Anyone who can reach
:8080can run a test. At ~1 GiB per/download?bytes=and 4 parallel browser streams, a single visitor draws ~4 GiB of egress per test. A modest pool of abusers can saturate your uplink. - No global rate limiting. A reverse proxy (nginx/Caddy/Traefik)
with
limit_req/ a token bucket is the recommended fix. X-Forwarded-Foris not trusted by default. See--trust-proxy-headers— only enable it when behind a proxy that strips and re-sets the header itself, otherwise any caller can spoof their IP into your/api/inforesponse and your ipinfo.io quota.
If you're running this on a homelab / LAN: ignore all of the above. The defaults are fine.
If you're publishing it on the internet: put it behind a reverse proxy with rate limits and (ideally) a WAF, or restrict access by firewall to known networks.
Safety caps¶
The handlers cap individual request sizes so a malicious or buggy client cannot make the server stream forever:
| Limit | Default | Purpose |
|---|---|---|
MaxDownloadMiB |
1024 MiB |
A ?bytes= value above this is silently capped. |
MaxUploadMiB |
1024 MiB |
Body bytes beyond this are dropped (http.MaxBytesReader). |
ReadTimeout |
60s |
Per-request read deadline. |
These are not currently exposed as CLI flags. To change them, set the
corresponding fields on server.Config from your own embedding of the
package, or open an issue / PR.
CORS and cache control¶
The server sets:
Access-Control-Allow-Origin: *on every response. The speed test isn't authenticated; the UI is served from the same origin in normal use.Cache-Control: no-store, no-cache, must-revalidateon/ping,/download,/upload, and/api/infoso intermediate proxies can't serve stale data and falsely inflate measurements.- Static UI assets (
/,index.html,style.css,app.js) are cacheable — they don't affect measurement accuracy.
OPTIONS requests return 204 No Content for CORS preflight.