Skip to content

Performance: JSON API responses are served uncompressed (history payload is large on slow links) #23

Description

@LarsLaskowski

Source: Project review 2026-07-13 (Performance / Security / Robustness), finding P1.
Category: performance · Severity: medium · Effort: medium

Problem

The HTTP layer (internal/httpapi/handlers.gowriteJSON) 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:

  1. 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).
  2. 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).
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions