Skip to content

Commit 76c1b47

Browse files
committed
Refactor rate limiting to use separate read/write Redis operations
1 parent 1bfcd37 commit 76c1b47

File tree

1 file changed

+40
-7
lines changed
  • packages/service-utils/src/core/rateLimit

1 file changed

+40
-7
lines changed

packages/service-utils/src/core/rateLimit/index.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,40 @@ const RATE_LIMIT_WINDOW_SECONDS = 10;
55

66
// Redis interface compatible with ioredis (Node) and upstash (Cloudflare Workers).
77
type IRedis = {
8-
incr: (key: string) => Promise<number>;
9-
expire: (key: string, ttlSeconds: number) => Promise<0 | 1>;
8+
get: (key: string) => Promise<string | null>;
9+
set(
10+
key: string,
11+
value: string | number,
12+
secondsToken?: "EX" | "XX",
13+
seconds?: number | string,
14+
): Promise<"OK">;
1015
};
1116

1217
export async function rateLimit(args: {
1318
team: TeamResponse;
1419
limitPerSecond: number;
1520
serviceConfig: CoreServiceConfig;
1621
redis: IRedis;
22+
readRedis?: IRedis;
1723
/**
1824
* Sample requests to reduce load on Redis.
1925
* This scales down the request count and the rate limit threshold.
2026
* @default 1.0
2127
*/
2228
sampleRate?: number;
29+
logger?: typeof console;
2330
}): Promise<RateLimitResult> {
24-
const { team, limitPerSecond, serviceConfig, redis, sampleRate = 1.0 } = args;
31+
const {
32+
team,
33+
limitPerSecond,
34+
serviceConfig,
35+
redis,
36+
readRedis,
37+
sampleRate = 1.0,
38+
} = args;
39+
40+
// fall back to the write redis if read redis is not provided explicitly
41+
const readOnlyRedis = readRedis ?? redis;
2542

2643
const shouldSampleRequest = Math.random() < sampleRate;
2744
if (!shouldSampleRequest) {
@@ -49,11 +66,27 @@ export async function rateLimit(args: {
4966
RATE_LIMIT_WINDOW_SECONDS;
5067
const key = `rate-limit:${serviceScope}:${team.id}:${timestampWindow}`;
5168

52-
// Increment and get the current request count in this window.
53-
const requestCount = await redis.incr(key);
69+
// first read the request count from redis
70+
const currentRequestCountFromRedis = Number(
71+
(await readOnlyRedis.get(key)) || "0",
72+
);
73+
74+
const requestCount = currentRequestCountFromRedis + 1;
75+
76+
// we are setting the request count, however we are not waiting on this to be complete
5477
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);
78+
// For the first increment, set an expiration to clean up this key (EX).
79+
redis
80+
.set(key, requestCount, "EX", RATE_LIMIT_WINDOW_SECONDS)
81+
.catch((err) => {
82+
console.error("failed to set request count", err);
83+
});
84+
} else {
85+
// For all other increments, just increment the request count.
86+
// only set it if it already exists (XX)
87+
redis.set(key, requestCount, "XX").catch((err) => {
88+
console.error("failed to increment request count", err);
89+
});
5790
}
5891

5992
// Get the limit for this window accounting for the sample rate.

0 commit comments

Comments
 (0)