Skip to content

Admin: per-account rate-limit hit history#136

Merged
SiteRelEnby merged 1 commit into
mainfrom
admin-rate-limit-history
Jun 12, 2026
Merged

Admin: per-account rate-limit hit history#136
SiteRelEnby merged 1 commit into
mainfrom
admin-rate-limit-history

Conversation

@SiteRelEnby

Copy link
Copy Markdown
Contributor

Closes out the admin batch 2 backlog. The Prometheus counters answer "is the instance being hammered"; this adds the per-account drill-down for abuse triage: "what has THIS account specifically tripped recently".

How it works

When a rate-limit check blocks a request that carries a resolved authenticated user, the limiter records {time, bucket, scope, route, ip} to a per-user Redis list (sheaf:rlh:{user_id}). Admins read it via GET /v1/admin/users/{id}/rate-limit-history (per-bucket summary + entries, newest first) and a new section in the Explain-account panel, which stays hidden when there is nothing to triage.

Bounds and failure modes

Storage is bounded twice: RATE_LIMIT_HISTORY_MAX_ENTRIES (default 200) per user via LTRIM, and RATE_LIMIT_HISTORY_HOURS (default 48) as a retention TTL, with RATE_LIMIT_HISTORY_ENABLED as the kill switch. Only blocked checks are recorded, so write volume is self-limiting at one entry per 429. The write is best-effort (a Redis hiccup can never turn a clean 429 into a 500), the read endpoint degrades to 503 if Redis is unreachable, and nothing touches Postgres. The read side filters entries older than the retention window by timestamp rather than trusting the key TTL, since a fresh hit refreshes the whole key.

Privacy posture

This is held personal data (IP + behaviour), so it follows the same rules as the rest: included in the admin DSAR dossier bundle, and purged outright on account deletion instead of waiting out the TTL. Anonymous traffic (failed logins from logged-out clients, the global per-IP backstop) is not attributable to an account and is never recorded; the aggregate metrics cover those. Reading the history writes no audit row, matching the existing read-only triage endpoints (explain, session list); documented in SELFHOSTING.md.

Tests

Four endpoint tests in the normal suite (seeded Redis round-trip incl. stale-entry filtering, empty history, 404, no-audit-row) plus an end-to-end test in the rate-limit config: an authenticated user trips the 5/60 per-user limit and the admin endpoint returns exactly one entry per 429 with the right scope/route/ip. Full multi-config run: all 9 configurations passed (1064 in the main config).

The aggregate Prometheus counters answer "is something being
hammered"; admins triaging a specific account had nothing answering
"what has THIS account tripped recently". Blocked rate-limit checks
attributable to an authenticated user are now recorded to a capped
per-user Redis list (sheaf:rlh:{user_id}) and exposed via
GET /v1/admin/users/{id}/rate-limit-history plus a section in the
Explain-account panel (per-bucket totals + recent entries, hidden
when clean).

Bounded twice: RATE_LIMIT_HISTORY_MAX_ENTRIES (200) per user and a
RATE_LIMIT_HISTORY_HOURS (48) retention TTL, with
RATE_LIMIT_HISTORY_ENABLED as the kill switch. Only blocked checks
record, so volume is self-limiting (one entry per 429); the write is
best-effort so a Redis blip can never turn a 429 into a 500; nothing
touches Postgres. The read side filters stale entries by timestamp
since a fresh hit refreshes the whole key's TTL.

Privacy: the history is held personal data (IP + behaviour), so it is
included in the admin DSAR dossier and purged on account deletion
rather than left to age out. Anonymous traffic (logged-out login
attempts, the global per-IP backstop) is not attributable to an
account and is never recorded; the metrics cover it in aggregate.
Reading the history writes no audit row, matching the other read-only
triage endpoints (explain, session list).
@SiteRelEnby SiteRelEnby enabled auto-merge June 12, 2026 04:40
@SiteRelEnby SiteRelEnby merged commit 13845a5 into main Jun 12, 2026
4 checks passed
@SiteRelEnby SiteRelEnby deleted the admin-rate-limit-history branch June 12, 2026 04:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant