Skip to content

feat(ratelimit): enforce limits across replicas sharing a database - #1339

Draft
SantiagoDePolonia wants to merge 2 commits into
mainfrom
feat/ratelimit-cluster-admission
Draft

SantiagoDePolonia wants to merge 2 commits into
mainfrom
feat/ratelimit-cluster-admission

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Description

  • Rate limits (requests, tokens, concurrency) now hold across all replicas sharing a database. Before, each replica admitted up to the full limit, so N replicas allowed about N times the limit.
  • Each writer's snapshot rows now hold only what that writer counted itself, never counts it loaded from others. The cluster count for a key is the sum of its rows with a matching epoch, each rolled forward to now. This replaces the max-merge restore. A restarted instance's earlier rows count as one more writer's, so each generation is counted once however many restarts there are. The legacy empty-writer row is one total, so it is summed like the others.
  • The limiter keeps a remote view per key: the other writers' summed counters, plus their live in-flight gauges. Admission, status, x-ratelimit-* headers, /v1/usage, and the routing probe all check local + remote. The remote view is refreshed on the flush loop (save, then load) and seeded from the load at Start. The request path never calls the database, and admission adds no allocations (perf guard passes).
  • The guarantee is approximate. A replica's counts reach its peers within about two flush intervals (one to save, one for peers to load), so the cluster can overshoot by about that much of the other replicas' traffic, plus requests admitted on several replicas at the same instant. The docs say so and point to sticky routing or a single replica for strict caps such as concurrent=1. A strict per-request mode is left for a follow-up.
  • Load cost: a flush writes only the counters that changed since the last flush (rows that didn't change keep their counts and still prune correctly after two periods). It reads only rows with updated_at at or after the previous load minus a 10s overlap, which covers slow commits and clock skew. Rows are merged into a cache keyed by row identity, with a full reload once a minute as a backstop. A new updated_at index serves this on SQLite, PostgreSQL, and MongoDB. Rows from dead writers stay cached until their windows roll out. A reset or epoch change clears local and remote counts for the key on every replica, and cached rows of the old epoch are skipped.
  • Concurrency: each writer publishes its in-flight count per key on every flush (new in_flight column/field), and publishes 0 once when the count drops. Peers count a row only while it is fresh: saved within max(3 × flush interval, 5s) by the reader's clock, so replica clocks should be NTP-synced. A crashed replica's slots therefore free within that bound. Stores keep in-flight rows for a day, so pruning never races the heartbeat. In-flight gauges ignore the epoch, matching how a local reset leaves them alone.
  • No new setting. RATE_LIMITS_FLUSH_INTERVAL sets the sharing cadence, and 0 means counts load at start only. A single instance behaves as before, except that after a crash (not a clean shutdown), a restart within ~5s sees the crashed run's last published in-flight slots until they go stale.
  • The schema change only adds a column to rate_limit_window_snapshots, the unreleased table from fix(ratelimit): keep per-replica counter snapshots so replicas stop overwriting each other #1296, plus an index. v0.2.0 never reads that table, so rolling upgrades are unaffected.

Tests: two-replica shared limit (requests, tokens, headers, routing probe), restart across four generations without inflation, reset and mode change clearing peer counts on every replica, peer in-flight counting and expiry on an injected clock, incremental load / full reload / roll-out eviction, failed-save retry, and the shared store suite (since filter, save stamps, in-flight round trip and retention) on SQLite, PostgreSQL and MongoDB. Mutation-checked: 15 targeted mutations, all caught. make test-race, lint, go fix, the hot-path perf guard and mint validate pass.

View guided diff Turn on auto-fix

@mintlify

mintlify Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
gomodel 🟢 Ready View Preview Oct 30, 2026, 2:39 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@coderabbitai

coderabbitai Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size:XL 500-999 effective changed lines (test files excluded in mixed PRs). label Oct 30, 2026
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 95.74468% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/ratelimit/peers.go 96.51% 3 Missing ⚠️
internal/ratelimit/persist.go 86.66% 2 Missing ⚠️
internal/ratelimit/snapshot.go 96.66% 2 Missing ⚠️
internal/ratelimit/store_sql.go 84.61% 2 Missing ⚠️
internal/ratelimit/store_mongodb.go 93.75% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

This branch was successfully deployed

1 active deployment
staging - docs — 802a0512 Deployed Oct 30, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 effective changed lines (test files excluded in mixed PRs).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants