11import type { CoreServiceConfig , TeamResponse } from "../api.js" ;
22import 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).
77type 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+ */
1219export 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