---
id: w7/m3/t003
title: "Request caps: body-size limit, log/metrics query-range bounds, SSE connection cap"
worker: worker7
status: done
estimate: 45m
depends_on: [w7/m3/t002]
---

## Objective

Bound the expensive request shapes rate-limiting alone doesn't catch — scoped (2026-07-11) to the gaps that actually remain: a global request body-size cap, explicit **time-window** bounds on log and metrics queries, an explicit **resolution** clamp for metrics, and a per-caller cap on concurrent SSE live-tail connections. Log **line-count** caps already exist and match Render (`lego/backend/internal/logs/service.go:49-50` — `defaultLogLimit=20`, `maxLogLimit=100`, clamped in `normalized()`; `loki.go` applies a default lookback + line cap) — don't re-build or accidentally change them.

## Context

- Body cap: `http.MaxBytesReader` at the same mux as t002's middleware (`lego/backend/internal/api`).
- Log queries + `/v1/logs/subscribe` SSE live in `lego/backend/internal/logs`; metrics ranges in `lego/backend/internal/metrics` (Prometheus-backed since w3/m4 — an unbounded range is a Prometheus-sized query).
- The real gaps (verified 2026-07-11): an old explicit `since` on logs is still an unbounded Loki query (only line counts are clamped); `lego/backend/internal/metrics/service.go:106-110` only defaults **zero** values — an explicit 1-year range at 1s resolution passes straight through to Prometheus. SSE has no per-caller connection cap; there is no body cap.
- Refusals need clear, Render-consistent 4xx errors (400/413), not silent truncation — except where Render itself silently caps (check t001's capture; follow it).
- Existing dashboards (w5/m6 logs, w3/m3+ metrics) must stay within the caps — pick bounds above what the UI requests.

## Steps

1. Add the body cap at the router; audit multipart/webhook paths for a compatible limit.
2. Clamp/reject out-of-bounds **explicit** log time windows and metrics start/end/resolution (the zero-value defaults already exist — bound the non-zero path); cap SSE connections per caller with a clean close on excess. Leave the existing Render-matched log line clamp untouched.
3. Click through the dashboard's Logs + Metrics tabs against the capped API to confirm no regression.

## Files

- `lego/backend/internal/api/` (body cap)
- `lego/backend/internal/logs/` (range bounds + SSE cap)
- `lego/backend/internal/metrics/` (range bounds)

## Acceptance criteria

- [ ] Oversized body → 413; out-of-range log/metrics query → 400 (or documented clamp); N+1th SSE stream refused cleanly
- [ ] Dashboard Logs + Metrics tabs work unchanged under the caps
- [ ] Caps enumerated in docs/ADR006-bex-api.md §Rate limits

## Out of scope

- Per-plan differentiated caps (needs the usage/plan track; note in the doc)
