From 7461ddf8f770189c852f44bc16f5ea3b455abbe3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 Aug 2025 20:56:23 -0700 Subject: [PATCH] feat(rate-limits): make rate limits configurable via environment variables (#892) * feat(rate-limits): make rate limits configurable via environment variables * add defaults for CI --- apps/sim/lib/env.ts | 12 ++++++ apps/sim/services/queue/RateLimiter.test.ts | 8 ++-- apps/sim/services/queue/RateLimiter.ts | 42 +++++++++++++-------- apps/sim/services/queue/types.ts | 23 +++++++---- helm/sim/values.yaml | 12 ++++++ 5 files changed, 69 insertions(+), 28 deletions(-) diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index 9c4ac5dde..44b398a88 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -110,6 +110,18 @@ export const env = createEnv({ // Data Retention FREE_PLAN_LOG_RETENTION_DAYS: z.string().optional(), // Log retention days for free plan users + // Rate Limiting Configuration + RATE_LIMIT_WINDOW_MS: z.string().optional().default('60000'), // Rate limit window duration in milliseconds (default: 1 minute) + MANUAL_EXECUTION_LIMIT: z.string().optional().default('999999'), // Manual execution bypass value (effectively unlimited) + RATE_LIMIT_FREE_SYNC: z.string().optional().default('10'), // Free tier sync API executions per minute + RATE_LIMIT_FREE_ASYNC: z.string().optional().default('50'), // Free tier async API executions per minute + RATE_LIMIT_PRO_SYNC: z.string().optional().default('25'), // Pro tier sync API executions per minute + RATE_LIMIT_PRO_ASYNC: z.string().optional().default('200'), // Pro tier async API executions per minute + RATE_LIMIT_TEAM_SYNC: z.string().optional().default('75'), // Team tier sync API executions per minute + RATE_LIMIT_TEAM_ASYNC: z.string().optional().default('500'), // Team tier async API executions per minute + RATE_LIMIT_ENTERPRISE_SYNC: z.string().optional().default('150'), // Enterprise tier sync API executions per minute + RATE_LIMIT_ENTERPRISE_ASYNC: z.string().optional().default('1000'), // Enterprise tier async API executions per minute + // Real-time Communication SOCKET_SERVER_URL: z.string().url().optional(), // WebSocket server URL for real-time features SOCKET_PORT: z.number().optional(), // Port for WebSocket server diff --git a/apps/sim/services/queue/RateLimiter.test.ts b/apps/sim/services/queue/RateLimiter.test.ts index 5bee99252..6fabed253 100644 --- a/apps/sim/services/queue/RateLimiter.test.ts +++ b/apps/sim/services/queue/RateLimiter.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { RateLimiter } from '@/services/queue/RateLimiter' -import { RATE_LIMITS } from '@/services/queue/types' +import { MANUAL_EXECUTION_LIMIT, RATE_LIMITS } from '@/services/queue/types' // Mock the database module vi.mock('@/db', () => ({ @@ -34,7 +34,7 @@ describe('RateLimiter', () => { const result = await rateLimiter.checkRateLimit(testUserId, 'free', 'manual', false) expect(result.allowed).toBe(true) - expect(result.remaining).toBe(999999) + expect(result.remaining).toBe(MANUAL_EXECUTION_LIMIT) expect(result.resetAt).toBeInstanceOf(Date) expect(db.select).not.toHaveBeenCalled() }) @@ -144,8 +144,8 @@ describe('RateLimiter', () => { const status = await rateLimiter.getRateLimitStatus(testUserId, 'free', 'manual', false) expect(status.used).toBe(0) - expect(status.limit).toBe(999999) - expect(status.remaining).toBe(999999) + expect(status.limit).toBe(MANUAL_EXECUTION_LIMIT) + expect(status.remaining).toBe(MANUAL_EXECUTION_LIMIT) expect(status.resetAt).toBeInstanceOf(Date) }) diff --git a/apps/sim/services/queue/RateLimiter.ts b/apps/sim/services/queue/RateLimiter.ts index a95a3b520..b6fa7935f 100644 --- a/apps/sim/services/queue/RateLimiter.ts +++ b/apps/sim/services/queue/RateLimiter.ts @@ -2,7 +2,13 @@ import { eq, sql } from 'drizzle-orm' import { createLogger } from '@/lib/logs/console/logger' import { db } from '@/db' import { userRateLimits } from '@/db/schema' -import { RATE_LIMITS, type SubscriptionPlan, type TriggerType } from '@/services/queue/types' +import { + MANUAL_EXECUTION_LIMIT, + RATE_LIMIT_WINDOW_MS, + RATE_LIMITS, + type SubscriptionPlan, + type TriggerType, +} from '@/services/queue/types' const logger = createLogger('RateLimiter') @@ -21,8 +27,8 @@ export class RateLimiter { if (triggerType === 'manual') { return { allowed: true, - remaining: 999999, - resetAt: new Date(Date.now() + 60000), + remaining: MANUAL_EXECUTION_LIMIT, + resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS), } } @@ -32,7 +38,7 @@ export class RateLimiter { : limit.syncApiExecutionsPerMinute const now = new Date() - const windowStart = new Date(now.getTime() - 60000) // 1 minute ago + const windowStart = new Date(now.getTime() - RATE_LIMIT_WINDOW_MS) // Get or create rate limit record const [rateLimitRecord] = await db @@ -78,7 +84,9 @@ export class RateLimiter { // Check if we exceeded the limit if (actualCount > execLimit) { - const resetAt = new Date(new Date(insertedRecord.windowStart).getTime() + 60000) + const resetAt = new Date( + new Date(insertedRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS + ) await db .update(userRateLimits) @@ -98,7 +106,7 @@ export class RateLimiter { return { allowed: true, remaining: execLimit - actualCount, - resetAt: new Date(new Date(insertedRecord.windowStart).getTime() + 60000), + resetAt: new Date(new Date(insertedRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS), } } @@ -124,7 +132,9 @@ export class RateLimiter { // Check if we exceeded the limit AFTER the atomic increment if (actualNewRequests > execLimit) { - const resetAt = new Date(new Date(rateLimitRecord.windowStart).getTime() + 60000) + const resetAt = new Date( + new Date(rateLimitRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS + ) logger.info( `Rate limit exceeded - request ${actualNewRequests} > limit ${execLimit} for user ${userId}`, @@ -154,7 +164,7 @@ export class RateLimiter { return { allowed: true, remaining: execLimit - actualNewRequests, - resetAt: new Date(new Date(rateLimitRecord.windowStart).getTime() + 60000), + resetAt: new Date(new Date(rateLimitRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS), } } catch (error) { logger.error('Error checking rate limit:', error) @@ -162,7 +172,7 @@ export class RateLimiter { return { allowed: true, remaining: 0, - resetAt: new Date(Date.now() + 60000), + resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS), } } } @@ -181,9 +191,9 @@ export class RateLimiter { if (triggerType === 'manual') { return { used: 0, - limit: 999999, - remaining: 999999, - resetAt: new Date(Date.now() + 60000), + limit: MANUAL_EXECUTION_LIMIT, + remaining: MANUAL_EXECUTION_LIMIT, + resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS), } } @@ -192,7 +202,7 @@ export class RateLimiter { ? limit.asyncApiExecutionsPerMinute : limit.syncApiExecutionsPerMinute const now = new Date() - const windowStart = new Date(now.getTime() - 60000) + const windowStart = new Date(now.getTime() - RATE_LIMIT_WINDOW_MS) const [rateLimitRecord] = await db .select() @@ -205,7 +215,7 @@ export class RateLimiter { used: 0, limit: execLimit, remaining: execLimit, - resetAt: new Date(now.getTime() + 60000), + resetAt: new Date(now.getTime() + RATE_LIMIT_WINDOW_MS), } } @@ -214,7 +224,7 @@ export class RateLimiter { used, limit: execLimit, remaining: Math.max(0, execLimit - used), - resetAt: new Date(new Date(rateLimitRecord.windowStart).getTime() + 60000), + resetAt: new Date(new Date(rateLimitRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS), } } catch (error) { logger.error('Error getting rate limit status:', error) @@ -225,7 +235,7 @@ export class RateLimiter { used: 0, limit: execLimit, remaining: execLimit, - resetAt: new Date(Date.now() + 60000), + resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS), } } } diff --git a/apps/sim/services/queue/types.ts b/apps/sim/services/queue/types.ts index 58eb7e5ff..dff5ed787 100644 --- a/apps/sim/services/queue/types.ts +++ b/apps/sim/services/queue/types.ts @@ -1,4 +1,5 @@ import type { InferSelectModel } from 'drizzle-orm' +import { env } from '@/lib/env' import type { userRateLimits } from '@/db/schema' // Database types @@ -16,22 +17,28 @@ export interface RateLimitConfig { asyncApiExecutionsPerMinute: number } +// Rate limit window duration in milliseconds +export const RATE_LIMIT_WINDOW_MS = Number.parseInt(env.RATE_LIMIT_WINDOW_MS) || 60000 + +// Manual execution bypass value (effectively unlimited) +export const MANUAL_EXECUTION_LIMIT = Number.parseInt(env.MANUAL_EXECUTION_LIMIT) || 999999 + export const RATE_LIMITS: Record = { free: { - syncApiExecutionsPerMinute: 10, - asyncApiExecutionsPerMinute: 50, + syncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_FREE_SYNC) || 10, + asyncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_FREE_ASYNC) || 50, }, pro: { - syncApiExecutionsPerMinute: 25, - asyncApiExecutionsPerMinute: 200, + syncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_PRO_SYNC) || 25, + asyncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_PRO_ASYNC) || 200, }, team: { - syncApiExecutionsPerMinute: 75, - asyncApiExecutionsPerMinute: 500, + syncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_TEAM_SYNC) || 75, + asyncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_TEAM_ASYNC) || 500, }, enterprise: { - syncApiExecutionsPerMinute: 150, - asyncApiExecutionsPerMinute: 1000, + syncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_ENTERPRISE_SYNC) || 150, + asyncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_ENTERPRISE_ASYNC) || 1000, }, } diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml index 1d364b862..201f4f39d 100644 --- a/helm/sim/values.yaml +++ b/helm/sim/values.yaml @@ -87,6 +87,18 @@ app: NEXT_PUBLIC_DOCUMENTATION_URL: "" # Documentation URL (leave empty for none) NEXT_PUBLIC_TERMS_URL: "" # Terms of service URL (leave empty for none) NEXT_PUBLIC_PRIVACY_URL: "" # Privacy policy URL (leave empty for none) + + # Rate Limiting Configuration + RATE_LIMIT_WINDOW_MS: "60000" # Rate limit window in milliseconds (1 minute) + MANUAL_EXECUTION_LIMIT: "999999" # Manual execution limit (effectively unlimited) + RATE_LIMIT_FREE_SYNC: "10" # Free tier sync API executions per minute + RATE_LIMIT_FREE_ASYNC: "50" # Free tier async API executions per minute + RATE_LIMIT_PRO_SYNC: "25" # Pro tier sync API executions per minute + RATE_LIMIT_PRO_ASYNC: "200" # Pro tier async API executions per minute + RATE_LIMIT_TEAM_SYNC: "75" # Team tier sync API executions per minute + RATE_LIMIT_TEAM_ASYNC: "500" # Team tier async API executions per minute + RATE_LIMIT_ENTERPRISE_SYNC: "150" # Enterprise tier sync API executions per minute + RATE_LIMIT_ENTERPRISE_ASYNC: "1000" # Enterprise tier async API executions per minute # Service configuration service: