@@ -5,23 +5,40 @@ const RATE_LIMIT_WINDOW_SECONDS = 10;
55
66// Redis interface compatible with ioredis (Node) and upstash (Cloudflare Workers).
77type 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
1217export 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