From e5c8aec07d5f2c77c6aa042da852413f88369533 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 12 Feb 2026 19:16:28 -0800 Subject: [PATCH] Add rate limiting (3 tries, exponential backoff) --- apps/sim/tools/index.ts | 53 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 44247e2c3..1bcb37724 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -102,6 +102,50 @@ async function injectHostedKeyIfNeeded( return true // Bill the user } +/** + * Check if an error is a rate limit (throttling) error + */ +function isRateLimitError(error: unknown): boolean { + if (error && typeof error === 'object') { + const status = (error as { status?: number }).status + // 429 = Too Many Requests, 503 = Service Unavailable (sometimes used for rate limiting) + if (status === 429 || status === 503) return true + } + return false +} + +/** + * Execute a function with exponential backoff retry for rate limiting errors. + * Only used for hosted key requests. + */ +async function executeWithRetry( + fn: () => Promise, + requestId: string, + toolId: string, + maxRetries = 3, + baseDelayMs = 1000 +): Promise { + let lastError: unknown + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn() + } catch (error) { + lastError = error + + if (!isRateLimitError(error) || attempt === maxRetries) { + throw error + } + + const delayMs = baseDelayMs * Math.pow(2, attempt) + logger.warn(`[${requestId}] Rate limited for ${toolId}, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})`) + await new Promise((resolve) => setTimeout(resolve, delayMs)) + } + } + + throw lastError +} + /** * Calculate cost based on pricing model */ @@ -569,7 +613,14 @@ export async function executeTool( } // Execute the tool request directly (internal routes use regular fetch, external use SSRF-protected fetch) - const result = await executeToolRequest(toolId, tool, contextParams) + // Wrap with retry logic for hosted keys to handle rate limiting due to higher usage + const result = isUsingHostedKey + ? await executeWithRetry( + () => executeToolRequest(toolId, tool, contextParams), + requestId, + toolId + ) + : await executeToolRequest(toolId, tool, contextParams) // Apply post-processing if available and not skipped let finalResult = result