Decision record
ADR-0006: Rate-limit counters live in RTDB
ADR-0006: Rate-limit counters live in RTDB
Status: accepted · 2026-09-04
Context
The MCP service limits each token to 120 requests a minute, and /developers
says so. Until now the counter was a Map in the process, so on Cloud Run the
real limit was 120 × the number of instances serving that token — an unknown
number that changes with load. Phase 10 §4 called this out; Phase 11 §9 asks
for a durable counter before a second integrator builds against the figure.
Bladbase does not run Redis (CLAUDE.md), so the candidates were the stores it has: Firestore, RTDB, or a Cloud Tasks/Pub/Sub contrivance.
Decision
A fixed window per token — rateLimits/{tokenId}/{windowNumber} — incremented
by an RTDB transaction, on the same Admin SDK client the change signal uses.
The first hit of a window removes the previous one, so a key never holds more
than two small nodes. Clients can neither read nor write the path.
The limiter fails open to a per-instance count: a transaction that does
not commit within 1.5 s, or throws, is answered from a local window and the
decision is marked shared: false, which the service logs. Every response
carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset;
a refusal is 429 with retry-after.
Why not Firestore
A single hot document tops out near one sustained write a second, which is below the rate the limiter exists to police. Sharding the counter across documents trades that for a read fan-out on every request, for a value that is thrown away sixty seconds later. Firestore is for durable metadata; this is not that.
Why RTDB does not break its own rule
RTDB is reserved for ephemeral awareness and is never a source of truth. A
rate window is ephemeral by construction and nothing durable is derived from
it; when it is missing, the service degrades to the limit it had before this
ADR, not to a wrong answer. The path is server-written only, like changes.
Consequences
- One transaction round-trip per MCP request (single-digit milliseconds in region). The limiter’s timeout bounds the worst case.
- The published figure is now the actual figure across instances.
- The same primitive serves any other per-key limit that needs to be shared (uploads, sign-in attempts) without a new store.
- A future move to a regional Redis-compatible service, should Bladbase ever
adopt one, is a one-file change behind
createRateLimiter.
References
- Realtime Database — where the shared counter lives