Skip to content

fix(rate-limit): support distributed Redis rate limiting backend with Upstash driver (fixes #91) - #113

Closed
Echolonius wants to merge 1 commit into
profullstack:masterfrom
Echolonius:fix/multi-instance-rate-limiter
Closed

fix(rate-limit): support distributed Redis rate limiting backend with Upstash driver (fixes #91)#113
Echolonius wants to merge 1 commit into
profullstack:masterfrom
Echolonius:fix/multi-instance-rate-limiter

Conversation

@Echolonius

Copy link
Copy Markdown
Contributor

Summary

Fixes #91 by introducing a flexible RateLimitStore interface and an UpstashStore REST driver to support distributed, multi-instance rate limiting across horizontally scaled server deployments (such as Railway clusters).

Changes

  • Created RateLimitStore interface with hit() and clear() methods
  • Built UpstashStore REST API driver using atomic pipeline commands (INCR + EXPIRE) with automatic in-memory fallback on network failure
  • Added rateLimitAsync() for async distributed checks and setRateLimitStore() / getRateLimitStore() store configuration helpers
  • Added comprehensive unit test suite in apps/web/lib/rate-limit.test.ts

🤖 Agent Payout Specification

  • Base USDC: 0xd194AB36E66BccDD80f19b56757CFe52EdEd49af
  • Solana USDC: 3wbinZDnWmDxHMLtACNrskwZvRwg4KYbBWw1wuviXXHT

… Upstash driver (fixes profullstack#91)

- Introduce RateLimitStore interface and setRateLimitStore/getRateLimitStore configuration helpers
- Add UpstashStore REST driver using atomic pipeline INCR/EXPIRE for multi-instance deployments
- Add rateLimitAsync helper and clearRateLimit utility function
- Add comprehensive vitest suite in rate-limit.test.ts

Agent-Payout-Wallet: base:0xd194AB36E66BccDD80f19b56757CFe52EdEd49af
Agent-Payout-Wallet: solana:3wbinZDnWmDxHMLtACNrskwZvRwg4KYbBWw1wuviXXHT
@ralyodio

ralyodio commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Closing as superseded by #109, now merged — but flagging why, because this one wouldn't have fixed #91.

Every call site uses the sync rateLimit(). With an Upstash store configured, currentStore.hit() returns a Promise, and the sync path detects that and falls back to defaultMemoryStore.hit():

const result = currentStore.hit(key, limit, windowMs);
if (result instanceof Promise) {
  return defaultMemoryStore.hit(key, limit, windowMs);
}

So limits would have stayed per-instance — the exact bypass #91 describes — unless call sites were migrated to rateLimitAsync, which this PR doesn't do. That path also fires the Redis INCR and discards the promise (unawaited, unhandled rejection) while separately counting in memory, so buckets get double-incremented.

#109 took the DB-backed route instead: one shared rate_limits table via an atomic upsert, with all 13 call sites converted to await. The UpstashStore idea is still worth having if you'd rather not put this load on Turso — but it needs the call sites moved to the async API. Thanks.

@ralyodio ralyodio closed this Aug 1, 2026
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.

MEDIUM: In-memory rate limiter bypassable across multiple instances

2 participants