Skip to content

Commit fda665b

Browse files
committed
Use atomic increments for rate limit counters
1 parent bfcb507 commit fda665b

1 file changed

Lines changed: 20 additions & 25 deletions

File tree

src/lib/rate-limit.ts

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { doc, runTransaction } from 'firebase/firestore';
1+
import { doc, getDoc, increment, setDoc } from 'firebase/firestore';
22
import { getDb } from './db';
33

44
interface RateLimitOptions {
@@ -26,31 +26,26 @@ export async function enforceRateLimit(
2626
{ windowMs = DEFAULT_WINDOW_MS, maxRequests = DEFAULT_MAX_REQUESTS }: RateLimitOptions = {}
2727
): Promise<RateLimitState> {
2828
const db = getDb();
29-
const ref = doc(db, 'rateLimits', key);
3029
const now = Date.now();
31-
32-
let state: RateLimitState = {
33-
allowed: true,
34-
remaining: maxRequests,
35-
reset: now + windowMs,
30+
const windowStart = Math.floor(now / windowMs) * windowMs;
31+
const ref = doc(db, 'rateLimits', `${key}:${windowStart}`);
32+
33+
await setDoc(
34+
ref,
35+
{ count: increment(1), windowStart },
36+
{ merge: true },
37+
);
38+
39+
const snapshot = await getDoc(ref);
40+
const data = snapshot.data() as RateLimitDocument | undefined;
41+
const count = data?.count ?? 0;
42+
const remaining = Math.max(0, maxRequests - count);
43+
const reset = windowStart + windowMs;
44+
45+
return {
46+
allowed: count <= maxRequests,
47+
remaining,
48+
reset,
3649
limit: maxRequests,
3750
};
38-
39-
await runTransaction(db, async (transaction) => {
40-
const snapshot = await transaction.get(ref);
41-
const data = snapshot.data() as RateLimitDocument | undefined;
42-
const withinWindow = data && now - data.windowStart < windowMs;
43-
44-
const windowStart = withinWindow ? data.windowStart : now;
45-
const newCount = (withinWindow ? data.count : 0) + 1;
46-
const remaining = Math.max(0, maxRequests - newCount);
47-
const reset = windowStart + windowMs;
48-
const allowed = newCount <= maxRequests;
49-
50-
transaction.set(ref, { count: newCount, windowStart });
51-
52-
state = { allowed, remaining, reset, limit: maxRequests };
53-
});
54-
55-
return state;
5651
}

0 commit comments

Comments
 (0)