Skip to content

Commit 6309f8a

Browse files
committed
chore: sliding window for rate limit
1 parent 7ab76ac commit 6309f8a

File tree

1 file changed

+44
-37
lines changed
  • packages/service-utils/src/core/rateLimit

1 file changed

+44
-37
lines changed
Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,28 @@
11
import type { CoreServiceConfig, TeamResponse } from "../api.js";
22
import type { RateLimitResult } from "./types.js";
33

4-
const RATE_LIMIT_WINDOW_SECONDS = 10;
4+
const SLIDING_WINDOW_SECONDS = 10;
55

66
// Redis interface compatible with ioredis (Node) and upstash (Cloudflare Workers).
77
type IRedis = {
88
incr: (key: string) => Promise<number>;
99
expire: (key: string, ttlSeconds: number) => Promise<0 | 1>;
10+
mget: (keys: string[]) => Promise<(string | null)[]>;
1011
};
1112

13+
/**
14+
* Increments the request count for this team and returns whether the team has hit their rate limit.
15+
* Uses a sliding 10 second window.
16+
* @param args
17+
* @returns
18+
*/
1219
export async function rateLimit(args: {
1320
team: TeamResponse;
1421
limitPerSecond: number;
1522
serviceConfig: CoreServiceConfig;
1623
redis: IRedis;
17-
/**
18-
* Sample requests to reduce load on Redis.
19-
* This scales down the request count and the rate limit threshold.
20-
* @default 1.0
21-
*/
22-
sampleRate?: number;
2324
}): Promise<RateLimitResult> {
24-
const { team, limitPerSecond, serviceConfig, redis, sampleRate = 1.0 } = args;
25-
26-
const shouldSampleRequest = Math.random() < sampleRate;
27-
if (!shouldSampleRequest) {
28-
return {
29-
rateLimited: false,
30-
requestCount: 0,
31-
rateLimit: 0,
32-
};
33-
}
25+
const { team, limitPerSecond, serviceConfig, redis } = args;
3426

3527
if (limitPerSecond === 0) {
3628
// No rate limit is provided. Assume the request is not rate limited.
@@ -41,39 +33,54 @@ export async function rateLimit(args: {
4133
};
4234
}
4335

36+
const currentSecond = Math.floor(Date.now() / 1000);
4437
const serviceScope = serviceConfig.serviceScope;
4538

46-
// Gets the 10-second window for the current timestamp.
47-
const timestampWindow =
48-
Math.floor(Date.now() / (1000 * RATE_LIMIT_WINDOW_SECONDS)) *
49-
RATE_LIMIT_WINDOW_SECONDS;
50-
const key = `rate-limit:${serviceScope}:${team.id}:${timestampWindow}`;
39+
// Increment the request count for the current second.
40+
const currentKey = getRequestCountAtSecondCacheKey(
41+
serviceScope,
42+
team.id,
43+
currentSecond,
44+
);
45+
await redis.incr(currentKey);
46+
// Expire this key after it's past the sliding window.
47+
await redis.expire(currentKey, SLIDING_WINDOW_SECONDS + 1);
5148

52-
// Increment and get the current request count in this window.
53-
const requestCount = await redis.incr(key);
54-
if (requestCount === 1) {
55-
// For the first increment, set an expiration to clean up this key.
56-
await redis.expire(key, RATE_LIMIT_WINDOW_SECONDS);
57-
}
49+
// Sum the request counts for the last `SLIDING_WINDOW_SECONDS` seconds.
50+
const keys = Array.from(
51+
{ length: SLIDING_WINDOW_SECONDS },
52+
(_, i) => `rate-limit:${serviceScope}:${team.id}:${currentSecond - i}`,
53+
);
54+
const counts = await redis.mget(keys);
55+
const totalRequests = counts.reduce(
56+
(sum, count) => sum + (count ? Number.parseInt(count) : 0),
57+
0,
58+
);
5859

59-
// Get the limit for this window accounting for the sample rate.
60-
const limitPerWindow =
61-
limitPerSecond * sampleRate * RATE_LIMIT_WINDOW_SECONDS;
60+
const limit = limitPerSecond * SLIDING_WINDOW_SECONDS;
6261

63-
if (requestCount > limitPerWindow) {
62+
if (totalRequests > limit) {
6463
return {
6564
rateLimited: true,
66-
requestCount,
67-
rateLimit: limitPerWindow,
65+
requestCount: totalRequests,
66+
rateLimit: limit,
6867
status: 429,
69-
errorMessage: `You've exceeded your ${serviceScope} rate limit at ${limitPerSecond} reqs/sec. To get higher rate limits, contact us at https://thirdweb.com/contact-us.`,
68+
errorMessage: `You've exceeded your ${serviceScope} rate limit at ${limitPerSecond} reqs/sec. Please upgrade your plan to get higher rate limits.`,
7069
errorCode: "RATE_LIMIT_EXCEEDED",
7170
};
7271
}
7372

7473
return {
7574
rateLimited: false,
76-
requestCount,
77-
rateLimit: limitPerWindow,
75+
requestCount: totalRequests,
76+
rateLimit: limit,
7877
};
7978
}
79+
80+
function getRequestCountAtSecondCacheKey(
81+
serviceScope: CoreServiceConfig["serviceScope"],
82+
teamId: string,
83+
second: number,
84+
) {
85+
return `rate-limit:${serviceScope}:${teamId}:${second}`;
86+
}

0 commit comments

Comments
 (0)