Skip to main content
ORVENATH
ADR-013|Architecture Decisions|Accepted
DECIDED: LAST UPDATED:

Rate limiting in Postgres rather than in memory

In-memory rate limiting does not function on serverless. State moved to the database.

Tags:#engineering#security#infrastructure

ADR-013 — Rate limiting in Postgres rather than in memory

Context#

The proposal share page exposes unauthenticated write endpoints — view recording and the duration beacon. Without limits, a single share token could be used to flood the proposal_views table.

The first implementation used an in-memory sliding window. This does not work on Vercel. Each serverless invocation may run in a separate instance with its own memory, and instances are recycled constantly. Counters do not accumulate across requests and vanish between them. The limiter appeared to work in local single-process tests and enforced effectively nothing in production.

This is a general failure mode worth recording: any state an in-memory implementation depends on is absent on serverless. It applies to caches, counters, sessions, and locks alike.

Decision#

Move rate limit state into Postgres, in the proposals schema:

CODE BLOCK // MONOReadOnly
proposals.rate_limit_buckets (
  key           text primary key,
  count         integer not null default 0,
  window_start  timestamptz not null default now()
)

Atomic increment via INSERT ... ON CONFLICT (key) DO UPDATE, resetting count when window_start falls outside the window. Keyed separately on share token and on IP hash. Rows older than the window are pruned on write.

Rejected: Upstash Redis. Correct for the job and faster, but adds a service, a second set of credentials, and another failure domain — for a system whose entire write volume is a handful of proposal views per week. Postgres already exists and is sufficient at this scale.

Consequences#

  • Limits are actually enforced across invocations
  • One database round-trip added per tracking request — negligible at this volume
  • Table requires periodic pruning, handled on write
  • Correctness verified across processes, not within one

Revisit when#

Tracking write volume makes per-request database round-trips material, or a system needs sub-millisecond limiting. Neither is close.