@omnifolio/rate-governor
v1.0.0
Published
Production-grade rate limiter with in-memory + Redis support, subscription tiers, automatic cleanup, and request deduplication. Battle-tested at scale on Cloud Run.
Maintainers
Readme
@omnifolio/rate-governor
Production-grade rate limiter with in-memory + Redis support, subscription tiers, automatic cleanup, and IP extraction helpers. Battle-tested at scale on Google Cloud Run.
Built by OmniFolio — Financial Intelligence Platform.
Features
- 🚀 Zero dependencies — works out of the box with just Node.js
- 🧠 In-memory by default — no Redis needed for single-instance deployments
- 🔌 Pluggable Redis adapter — bring your own Redis client (Upstash, ioredis, etc.)
- ⚡ Atomic Redis operations — single INCR+EXPIRE round-trip on the hot path
- 🛡️ Block mode — temporarily ban abusive clients after limit exceeded
- 💳 Subscription tiers — scale limits by plan (free → pro → enterprise)
- 📊 Standard headers —
X-RateLimit-Limit,X-RateLimit-Remaining,Retry-After - 🧹 Auto-cleanup — stale entries evicted every 60s, emergency eviction at 10K entries
- 🌐 IP extraction — handles Cloudflare, AWS ALB, GCP, nginx proxy headers
Install
npm install @omnifolio/rate-governorQuick Start
Basic (in-memory)
import { checkRateLimit, Presets, getClientIP } from '@omnifolio/rate-governor';
// In your API route handler:
const ip = getClientIP(request.headers);
const result = checkRateLimit(`ip:${ip}`, Presets.STANDARD);
if (!result.success) {
return new Response(JSON.stringify({ error: 'Too many requests' }), {
status: 429,
headers: result.headers,
});
}
// Request allowed — continue processingWith Redis (multi-instance)
import { checkRateLimitAsync, Presets, buildIdentifier } from '@omnifolio/rate-governor';
import type { RedisAdapter } from '@omnifolio/rate-governor';
import { Redis } from '@upstash/redis';
// Create your Redis adapter
const redis = new Redis({ url: '...', token: '...' });
const adapter: RedisAdapter = {
async incrWithExpire(key, ttl) {
const pipeline = redis.pipeline();
pipeline.incr(key);
pipeline.expire(key, ttl);
const results = await pipeline.exec();
return (results[0] as number) ?? null;
},
async get<T>(key: string) {
return redis.get<T>(key);
},
async set<T>(key: string, value: T, ttl: number) {
await redis.set(key, value, { ex: ttl });
},
};
// In your handler:
const id = buildIdentifier(request.headers, session?.userId);
const result = await checkRateLimitAsync(id, Presets.API_DATA, adapter);
if (!result.success) {
return new Response('Rate limited', { status: 429, headers: result.headers });
}Subscription Tiers
import { Presets, getSubscriptionRateLimit, checkRateLimit } from '@omnifolio/rate-governor';
// Scale limits based on user's plan
const config = getSubscriptionRateLimit(Presets.API_DATA, user.plan);
// Free: 100 req/min | Plus: 200 | Pro: 500 | Enterprise: 1000
const result = checkRateLimit(`user:${user.id}`, config);Presets
| Preset | Limit | Window | Block | Use Case |
|--------|-------|--------|-------|----------|
| STANDARD | 60/min | 1 min | 1 min | General API |
| GENEROUS | 120/min | 1 min | — | Read-heavy |
| API_DATA | 100/min | 1 min | 1 min | Authenticated data |
| AI | 30/min | 1 min | 2 min | LLM / AI ops |
| STRICT | 10/min | 1 min | 5 min | Sensitive ops |
| AUTH | 5/15 min | 15 min | 30 min | Login/register |
| WEBHOOK | 1000/min | 1 min | — | Trusted webhooks |
| PUBLIC_HOURLY | 1000/hr | 1 hr | — | Public API |
| PUBLIC_READ | 200/min | 1 min | — | Public reads |
API Reference
checkRateLimit(identifier, config)
Synchronous in-memory rate limit check. Returns RateLimitResult.
checkRateLimitAsync(identifier, config, redis)
Async rate limit check using a RedisAdapter. Falls back to in-memory on failure.
getClientIP(headers)
Extract client IP from proxy headers (Cloudflare, ALB, nginx).
buildIdentifier(headers, userId?)
Build a rate limit key: user:{id} if authenticated, ip:{ip} otherwise.
getSubscriptionRateLimit(config, plan)
Scale a config's limit by the plan multiplier.
blockIdentifier(identifier, durationMs?)
Manually block a client for a duration (default: 1 hour).
unblockIdentifier(identifier)
Remove a manual block.
getStats()
Returns { totalClients, blockedClients } from the in-memory store.
clearStore()
Clear all rate limit records (useful in tests).
Subscription Multipliers
| Plan | Multiplier | |------|-----------| | Free (default) | 1× | | Plus / Trader | 2× | | Pro / Investor | 5× | | Enterprise / Whale | 10× |
License
MIT — see LICENSE.
Built with ❤️ by OmniFolio
