Source: Project review 2026-07-13 (Performance / Security / Robustness), finding P1.
Category: performance · Severity: medium · Effort: medium
Problem
The HTTP layer (internal/httpapi/handlers.go → writeJSON) never compresses responses. With default config (60-minute window at 5-second polling = 720 points per series), GET /api/v1/metrics/history returns 7 fixed series plus one per mountpoint and two per network interface, each point serialized as {"t":"2026-07-12T18:00:00.123456789+02:00","v":12.4} (~50 bytes). A typical Pi with 2 disks and 2 interfaces produces roughly 13 series × 720 points ≈ 450 KB of JSON, re-sent in full every poll (dashboard: every 60 s; third-party pollers possibly more often). On a Pi Zero over Wi-Fi this is noticeable both in transfer time and CPU. Highly repetitive JSON like this compresses ~10×.
GET /api/v1/metrics is smaller but also repetitive (per-core arrays, package lists) and benefits too.
Fix
Add transparent gzip support using only the standard library:
- In
internal/httpapi/middleware.go, add a withGzip middleware:
- If the request's
Accept-Encoding does not contain gzip, pass through unchanged.
- Otherwise set
Content-Encoding: gzip and Vary: Accept-Encoding, delete any Content-Length, and wrap the ResponseWriter so Write goes through a *gzip.Writer (compress/gzip). Flush/Close the gzip writer after next.ServeHTTP returns.
- Pool gzip writers with a
sync.Pool to avoid per-request allocation; gzip.NewWriterLevel(w, gzip.BestSpeed) is the right trade-off on a Pi.
- The wrapper must still forward
WriteHeader (the logging middleware's statusRecorder pattern in the same file shows how).
- In
internal/httpapi/server.go, apply it to the API routes (wrapping the whole mux is fine too, but skip already-compressed content; the embedded assets are text, so compressing everything is acceptable and simplest).
- Do not compress
/healthz (2-byte body; harmless either way — implementer's choice).
Acceptance criteria
curl -sH 'Accept-Encoding: gzip' -o /dev/null -w '%{size_download}\n' http://localhost:8080/api/v1/metrics/history is roughly an order of magnitude smaller than without the header, and the decompressed body is byte-identical JSON.
- Requests without
Accept-Encoding: gzip receive identity responses (backward compatible for naive API clients — this must not break existing /api/v1 consumers).
- New handler test: request with
Accept-Encoding: gzip returns Content-Encoding: gzip and a body that gunzips to the same JSON as an uncompressed request.
go test ./... -race passes.
Source: Project review 2026-07-13 (Performance / Security / Robustness), finding P1.
Category: performance · Severity: medium · Effort: medium
Problem
The HTTP layer (
internal/httpapi/handlers.go→writeJSON) never compresses responses. With default config (60-minute window at 5-second polling = 720 points per series),GET /api/v1/metrics/historyreturns 7 fixed series plus one per mountpoint and two per network interface, each point serialized as{"t":"2026-07-12T18:00:00.123456789+02:00","v":12.4}(~50 bytes). A typical Pi with 2 disks and 2 interfaces produces roughly 13 series × 720 points ≈ 450 KB of JSON, re-sent in full every poll (dashboard: every 60 s; third-party pollers possibly more often). On a Pi Zero over Wi-Fi this is noticeable both in transfer time and CPU. Highly repetitive JSON like this compresses ~10×.GET /api/v1/metricsis smaller but also repetitive (per-core arrays, package lists) and benefits too.Fix
Add transparent gzip support using only the standard library:
internal/httpapi/middleware.go, add awithGzipmiddleware:Accept-Encodingdoes not containgzip, pass through unchanged.Content-Encoding: gzipandVary: Accept-Encoding, delete anyContent-Length, and wrap theResponseWritersoWritegoes through a*gzip.Writer(compress/gzip). Flush/Close the gzip writer afternext.ServeHTTPreturns.sync.Poolto avoid per-request allocation;gzip.NewWriterLevel(w, gzip.BestSpeed)is the right trade-off on a Pi.WriteHeader(the logging middleware'sstatusRecorderpattern in the same file shows how).internal/httpapi/server.go, apply it to the API routes (wrapping the whole mux is fine too, but skip already-compressed content; the embedded assets are text, so compressing everything is acceptable and simplest)./healthz(2-byte body; harmless either way — implementer's choice).Acceptance criteria
curl -sH 'Accept-Encoding: gzip' -o /dev/null -w '%{size_download}\n' http://localhost:8080/api/v1/metrics/historyis roughly an order of magnitude smaller than without the header, and the decompressed body is byte-identical JSON.Accept-Encoding: gzipreceive identity responses (backward compatible for naive API clients — this must not break existing/api/v1consumers).Accept-Encoding: gzipreturnsContent-Encoding: gzipand a body that gunzips to the same JSON as an uncompressed request.go test ./... -racepasses.