@@ -34,10 +34,10 @@ const BASE_BATCH_DELAY = 1000;
3434const BATCH_SIZE = 100 ;
3535const EMAIL_POOL_SIZE = 4 ;
3636const CALLBACK_POOL_SIZE = 4 ;
37- const RESEND_ONE_CALL_EVERY_MS = 600 ; // Half the stated limit, but it keeps us sane.
3837const FINALIZED_EMAIL_RETENTION_MS = 1000 * 60 * 60 * 24 * 7 ; // 7 days
3938const FINALIZED_EPOCH = Number . MAX_SAFE_INTEGER ;
4039const ABANDONED_EMAIL_RETENTION_MS = 1000 * 60 * 60 * 24 * 30 ; // 30 days
40+ const DEFAULT_RATE_LIMIT_PER_SECOND = 2 ;
4141
4242const RESEND_TEST_EMAILS = [ "delivered" , "bounced" , "complained" ] ;
4343
@@ -70,7 +70,7 @@ function getSegment(now: number) {
7070 return Math . floor ( now / SEGMENT_MS ) ;
7171}
7272
73- // Four threads is more than enough, especially given the low rate limiting .
73+ // Four threads is more than enough for the email sending workpool .
7474const emailPool = new Workpool ( components . emailWorkpool , {
7575 maxParallelism : EMAIL_POOL_SIZE ,
7676} ) ;
@@ -81,14 +81,7 @@ const callbackPool = new Workpool(components.callbackWorkpool, {
8181} ) ;
8282
8383// We rate limit our calls to the Resend API.
84- // FUTURE -- make this rate configurable if an account ups its sending rate with Resend.
85- const resendApiRateLimiter = new RateLimiter ( components . rateLimiter , {
86- resendApi : {
87- kind : "fixed window" ,
88- period : RESEND_ONE_CALL_EVERY_MS ,
89- rate : 1 ,
90- } ,
91- } ) ;
84+ const resendApiRateLimiter = new RateLimiter ( components . rateLimiter ) ;
9285
9386// Periodic background job to clean up old emails that have already
9487// been delivered, bounced, what have you.
@@ -443,7 +436,12 @@ export const makeBatch = internalMutation({
443436 }
444437
445438 // Okay, let's calculate rate limiting as best we can globally in this distributed system.
446- const delay = await getDelay ( ctx ) ;
439+ // Handle backward compatibility: old database records may not have rateLimitPerSecond.
440+ // Use fallback to default (2 req/s) for migration safety.
441+ const delay = await getDelay (
442+ ctx ,
443+ options . rateLimitPerSecond ?? DEFAULT_RATE_LIMIT_PER_SECOND ,
444+ ) ;
447445
448446 // Give the batch to the workpool! It will call the Resend batch API
449447 // in a durable background action.
@@ -707,9 +705,23 @@ async function createResendBatchPayload(
707705}
708706
709707const FIXED_WINDOW_DELAY = 100 ;
710- async function getDelay ( ctx : RunMutationCtx & RunQueryCtx ) : Promise < number > {
708+ async function getDelay (
709+ ctx : RunMutationCtx & RunQueryCtx ,
710+ rateLimitPerSecond : number ,
711+ ) : Promise < number > {
712+ // Calculate the period in milliseconds based on the configured rate limit.
713+ // We use a conservative approach: allow 1 request per period.
714+ // For example, 100 requests/second = 10ms period, 2 requests/second = 500ms period.
715+ // Ensure minimum period of 1ms to allow high rate limits.
716+ const periodMs = Math . max ( 1 , Math . floor ( 1000 / rateLimitPerSecond ) ) ;
717+
711718 const limit = await resendApiRateLimiter . limit ( ctx , "resendApi" , {
712719 reserve : true ,
720+ config : {
721+ kind : "fixed window" ,
722+ period : periodMs ,
723+ rate : 1 ,
724+ } ,
713725 } ) ;
714726 //console.log(`RL: ${limit.ok} ${limit.retryAfter}`);
715727 const jitter = Math . random ( ) * FIXED_WINDOW_DELAY ;
0 commit comments