Window-based rate limiting with atomic increment and probabilistic cleanup. Storage-agnostic — works with D1, SQLite, Redis, or in-memory. Zero dependencies.
npm install @arraypress/rate-limitimport { checkRateLimit, rateLimitKey, rateLimitError, createMemoryStore } from '@arraypress/rate-limit';
const store = createMemoryStore(); // Or your D1/Redis adapter
// Check rate limit: 3 requests per 5 minutes
const ip = request.headers.get('CF-Connecting-IP');
const result = await checkRateLimit(store, rateLimitKey(ip, '/login'), 3, 300);
if (!result.allowed) {
return new Response(JSON.stringify(rateLimitError(result.retryAfter)), {
status: 429,
headers: { 'Retry-After': String(result.retryAfter) },
});
}CREATE TABLE rate_limits (key TEXT, window TEXT, count INTEGER DEFAULT 1, PRIMARY KEY (key, window));
CREATE INDEX idx_rate_limits_window ON rate_limits(window);const d1Store = {
async increment(key, windowStart) {
const r = await db.prepare(
`INSERT INTO rate_limits (key, window, count) VALUES (?, ?, 1)
ON CONFLICT(key, window) DO UPDATE SET count = count + 1 RETURNING count`
).bind(key, windowStart).first();
return r.count;
},
async cleanup(cutoff) {
const r = await db.prepare('DELETE FROM rate_limits WHERE window < ?').bind(cutoff).run();
return r.changes;
},
};Check rate limit. Returns { allowed, count, limit, retryAfter }. Runs probabilistic cleanup (1% chance per call).
Build a composite key: '1.2.3.4:/login'.
Calculate the window start timestamp for time-bucketing.
Create a { error, message, retryAfter } response body.
In-memory store for testing and single-instance use.
interface RateLimitStore {
increment(key: string, windowStart: string): Promise<number>;
cleanup(cutoffTime: string): Promise<number>;
}MIT