Problem. /api/v1/* has no rate limiting at all (middleware chain router.go:73-78). The API contract defines a rate_limited error code (api_contract.md:786) that nothing emits. Third parties are reverse-proxying the API in bulk. beacon-web (dev, 2026-09-02) already handles a 429 with Retry-After, never retries 4xx, and shows a RATE LIMITED badge.
Ask.
github.com/go-chi/httprate in the global chain, after the client-IP middleware (see the trusted-proxies issue) and before Logger, so 429s are logged with the real IP.
- Config block
ratelimit: { enabled, requests_per_minute, burst }. Start generous, e.g. 300/min per IP on /api/v1/*; the edge logs show a real session peaks at ~150 requests in its first 30 s. In-memory is fine for a single instance; httprate-redis is a drop-in later.
- Custom
httprate.WithLimitHandler that writes the contract envelope {"error":{"code":"rate_limited","message":"..."}} with Content-Type: application/json, Retry-After in delta-seconds, and Access-Control-Expose-Headers: Retry-After (only matters cross-origin, e.g. Vite dev on 5173 without the proxy). Note respondError derives the code from the status text, which would give too_many_requests, so the handler must set the code itself.
- One log line per 429 with IP and path. The edge fail2ban
beacon-api-429 jail keys on the edge log's status, so nothing else is needed there.
Problem.
/api/v1/*has no rate limiting at all (middleware chainrouter.go:73-78). The API contract defines arate_limitederror code (api_contract.md:786) that nothing emits. Third parties are reverse-proxying the API in bulk. beacon-web (dev, 2026-09-02) already handles a 429 withRetry-After, never retries 4xx, and shows aRATE LIMITEDbadge.Ask.
github.com/go-chi/httpratein the global chain, after the client-IP middleware (see the trusted-proxies issue) and beforeLogger, so 429s are logged with the real IP.ratelimit: { enabled, requests_per_minute, burst }. Start generous, e.g. 300/min per IP on/api/v1/*; the edge logs show a real session peaks at ~150 requests in its first 30 s. In-memory is fine for a single instance;httprate-redisis a drop-in later.httprate.WithLimitHandlerthat writes the contract envelope{"error":{"code":"rate_limited","message":"..."}}withContent-Type: application/json,Retry-Afterin delta-seconds, andAccess-Control-Expose-Headers: Retry-After(only matters cross-origin, e.g. Vite dev on 5173 without the proxy). NoterespondErrorderives the code from the status text, which would givetoo_many_requests, so the handler must set the code itself.beacon-api-429jail keys on the edge log's status, so nothing else is needed there.