Skip to content

Commit 7461ddf

Browse files
authored
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
1 parent f94258e commit 7461ddf

File tree

5 files changed

+69
-28
lines changed

5 files changed

+69
-28
lines changed

apps/sim/lib/env.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,18 @@ export const env = createEnv({
110110
// Data Retention
111111
FREE_PLAN_LOG_RETENTION_DAYS: z.string().optional(), // Log retention days for free plan users
112112

113+
// Rate Limiting Configuration
114+
RATE_LIMIT_WINDOW_MS: z.string().optional().default('60000'), // Rate limit window duration in milliseconds (default: 1 minute)
115+
MANUAL_EXECUTION_LIMIT: z.string().optional().default('999999'), // Manual execution bypass value (effectively unlimited)
116+
RATE_LIMIT_FREE_SYNC: z.string().optional().default('10'), // Free tier sync API executions per minute
117+
RATE_LIMIT_FREE_ASYNC: z.string().optional().default('50'), // Free tier async API executions per minute
118+
RATE_LIMIT_PRO_SYNC: z.string().optional().default('25'), // Pro tier sync API executions per minute
119+
RATE_LIMIT_PRO_ASYNC: z.string().optional().default('200'), // Pro tier async API executions per minute
120+
RATE_LIMIT_TEAM_SYNC: z.string().optional().default('75'), // Team tier sync API executions per minute
121+
RATE_LIMIT_TEAM_ASYNC: z.string().optional().default('500'), // Team tier async API executions per minute
122+
RATE_LIMIT_ENTERPRISE_SYNC: z.string().optional().default('150'), // Enterprise tier sync API executions per minute
123+
RATE_LIMIT_ENTERPRISE_ASYNC: z.string().optional().default('1000'), // Enterprise tier async API executions per minute
124+
113125
// Real-time Communication
114126
SOCKET_SERVER_URL: z.string().url().optional(), // WebSocket server URL for real-time features
115127
SOCKET_PORT: z.number().optional(), // Port for WebSocket server

apps/sim/services/queue/RateLimiter.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest'
22
import { RateLimiter } from '@/services/queue/RateLimiter'
3-
import { RATE_LIMITS } from '@/services/queue/types'
3+
import { MANUAL_EXECUTION_LIMIT, RATE_LIMITS } from '@/services/queue/types'
44

55
// Mock the database module
66
vi.mock('@/db', () => ({
@@ -34,7 +34,7 @@ describe('RateLimiter', () => {
3434
const result = await rateLimiter.checkRateLimit(testUserId, 'free', 'manual', false)
3535

3636
expect(result.allowed).toBe(true)
37-
expect(result.remaining).toBe(999999)
37+
expect(result.remaining).toBe(MANUAL_EXECUTION_LIMIT)
3838
expect(result.resetAt).toBeInstanceOf(Date)
3939
expect(db.select).not.toHaveBeenCalled()
4040
})
@@ -144,8 +144,8 @@ describe('RateLimiter', () => {
144144
const status = await rateLimiter.getRateLimitStatus(testUserId, 'free', 'manual', false)
145145

146146
expect(status.used).toBe(0)
147-
expect(status.limit).toBe(999999)
148-
expect(status.remaining).toBe(999999)
147+
expect(status.limit).toBe(MANUAL_EXECUTION_LIMIT)
148+
expect(status.remaining).toBe(MANUAL_EXECUTION_LIMIT)
149149
expect(status.resetAt).toBeInstanceOf(Date)
150150
})
151151

apps/sim/services/queue/RateLimiter.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { eq, sql } from 'drizzle-orm'
22
import { createLogger } from '@/lib/logs/console/logger'
33
import { db } from '@/db'
44
import { userRateLimits } from '@/db/schema'
5-
import { RATE_LIMITS, type SubscriptionPlan, type TriggerType } from '@/services/queue/types'
5+
import {
6+
MANUAL_EXECUTION_LIMIT,
7+
RATE_LIMIT_WINDOW_MS,
8+
RATE_LIMITS,
9+
type SubscriptionPlan,
10+
type TriggerType,
11+
} from '@/services/queue/types'
612

713
const logger = createLogger('RateLimiter')
814

@@ -21,8 +27,8 @@ export class RateLimiter {
2127
if (triggerType === 'manual') {
2228
return {
2329
allowed: true,
24-
remaining: 999999,
25-
resetAt: new Date(Date.now() + 60000),
30+
remaining: MANUAL_EXECUTION_LIMIT,
31+
resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS),
2632
}
2733
}
2834

@@ -32,7 +38,7 @@ export class RateLimiter {
3238
: limit.syncApiExecutionsPerMinute
3339

3440
const now = new Date()
35-
const windowStart = new Date(now.getTime() - 60000) // 1 minute ago
41+
const windowStart = new Date(now.getTime() - RATE_LIMIT_WINDOW_MS)
3642

3743
// Get or create rate limit record
3844
const [rateLimitRecord] = await db
@@ -78,7 +84,9 @@ export class RateLimiter {
7884

7985
// Check if we exceeded the limit
8086
if (actualCount > execLimit) {
81-
const resetAt = new Date(new Date(insertedRecord.windowStart).getTime() + 60000)
87+
const resetAt = new Date(
88+
new Date(insertedRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS
89+
)
8290

8391
await db
8492
.update(userRateLimits)
@@ -98,7 +106,7 @@ export class RateLimiter {
98106
return {
99107
allowed: true,
100108
remaining: execLimit - actualCount,
101-
resetAt: new Date(new Date(insertedRecord.windowStart).getTime() + 60000),
109+
resetAt: new Date(new Date(insertedRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS),
102110
}
103111
}
104112

@@ -124,7 +132,9 @@ export class RateLimiter {
124132

125133
// Check if we exceeded the limit AFTER the atomic increment
126134
if (actualNewRequests > execLimit) {
127-
const resetAt = new Date(new Date(rateLimitRecord.windowStart).getTime() + 60000)
135+
const resetAt = new Date(
136+
new Date(rateLimitRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS
137+
)
128138

129139
logger.info(
130140
`Rate limit exceeded - request ${actualNewRequests} > limit ${execLimit} for user ${userId}`,
@@ -154,15 +164,15 @@ export class RateLimiter {
154164
return {
155165
allowed: true,
156166
remaining: execLimit - actualNewRequests,
157-
resetAt: new Date(new Date(rateLimitRecord.windowStart).getTime() + 60000),
167+
resetAt: new Date(new Date(rateLimitRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS),
158168
}
159169
} catch (error) {
160170
logger.error('Error checking rate limit:', error)
161171
// Allow execution on error to avoid blocking users
162172
return {
163173
allowed: true,
164174
remaining: 0,
165-
resetAt: new Date(Date.now() + 60000),
175+
resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS),
166176
}
167177
}
168178
}
@@ -181,9 +191,9 @@ export class RateLimiter {
181191
if (triggerType === 'manual') {
182192
return {
183193
used: 0,
184-
limit: 999999,
185-
remaining: 999999,
186-
resetAt: new Date(Date.now() + 60000),
194+
limit: MANUAL_EXECUTION_LIMIT,
195+
remaining: MANUAL_EXECUTION_LIMIT,
196+
resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS),
187197
}
188198
}
189199

@@ -192,7 +202,7 @@ export class RateLimiter {
192202
? limit.asyncApiExecutionsPerMinute
193203
: limit.syncApiExecutionsPerMinute
194204
const now = new Date()
195-
const windowStart = new Date(now.getTime() - 60000)
205+
const windowStart = new Date(now.getTime() - RATE_LIMIT_WINDOW_MS)
196206

197207
const [rateLimitRecord] = await db
198208
.select()
@@ -205,7 +215,7 @@ export class RateLimiter {
205215
used: 0,
206216
limit: execLimit,
207217
remaining: execLimit,
208-
resetAt: new Date(now.getTime() + 60000),
218+
resetAt: new Date(now.getTime() + RATE_LIMIT_WINDOW_MS),
209219
}
210220
}
211221

@@ -214,7 +224,7 @@ export class RateLimiter {
214224
used,
215225
limit: execLimit,
216226
remaining: Math.max(0, execLimit - used),
217-
resetAt: new Date(new Date(rateLimitRecord.windowStart).getTime() + 60000),
227+
resetAt: new Date(new Date(rateLimitRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS),
218228
}
219229
} catch (error) {
220230
logger.error('Error getting rate limit status:', error)
@@ -225,7 +235,7 @@ export class RateLimiter {
225235
used: 0,
226236
limit: execLimit,
227237
remaining: execLimit,
228-
resetAt: new Date(Date.now() + 60000),
238+
resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS),
229239
}
230240
}
231241
}

apps/sim/services/queue/types.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { InferSelectModel } from 'drizzle-orm'
2+
import { env } from '@/lib/env'
23
import type { userRateLimits } from '@/db/schema'
34

45
// Database types
@@ -16,22 +17,28 @@ export interface RateLimitConfig {
1617
asyncApiExecutionsPerMinute: number
1718
}
1819

20+
// Rate limit window duration in milliseconds
21+
export const RATE_LIMIT_WINDOW_MS = Number.parseInt(env.RATE_LIMIT_WINDOW_MS) || 60000
22+
23+
// Manual execution bypass value (effectively unlimited)
24+
export const MANUAL_EXECUTION_LIMIT = Number.parseInt(env.MANUAL_EXECUTION_LIMIT) || 999999
25+
1926
export const RATE_LIMITS: Record<SubscriptionPlan, RateLimitConfig> = {
2027
free: {
21-
syncApiExecutionsPerMinute: 10,
22-
asyncApiExecutionsPerMinute: 50,
28+
syncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_FREE_SYNC) || 10,
29+
asyncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_FREE_ASYNC) || 50,
2330
},
2431
pro: {
25-
syncApiExecutionsPerMinute: 25,
26-
asyncApiExecutionsPerMinute: 200,
32+
syncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_PRO_SYNC) || 25,
33+
asyncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_PRO_ASYNC) || 200,
2734
},
2835
team: {
29-
syncApiExecutionsPerMinute: 75,
30-
asyncApiExecutionsPerMinute: 500,
36+
syncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_TEAM_SYNC) || 75,
37+
asyncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_TEAM_ASYNC) || 500,
3138
},
3239
enterprise: {
33-
syncApiExecutionsPerMinute: 150,
34-
asyncApiExecutionsPerMinute: 1000,
40+
syncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_ENTERPRISE_SYNC) || 150,
41+
asyncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_ENTERPRISE_ASYNC) || 1000,
3542
},
3643
}
3744

helm/sim/values.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ app:
8787
NEXT_PUBLIC_DOCUMENTATION_URL: "" # Documentation URL (leave empty for none)
8888
NEXT_PUBLIC_TERMS_URL: "" # Terms of service URL (leave empty for none)
8989
NEXT_PUBLIC_PRIVACY_URL: "" # Privacy policy URL (leave empty for none)
90+
91+
# Rate Limiting Configuration
92+
RATE_LIMIT_WINDOW_MS: "60000" # Rate limit window in milliseconds (1 minute)
93+
MANUAL_EXECUTION_LIMIT: "999999" # Manual execution limit (effectively unlimited)
94+
RATE_LIMIT_FREE_SYNC: "10" # Free tier sync API executions per minute
95+
RATE_LIMIT_FREE_ASYNC: "50" # Free tier async API executions per minute
96+
RATE_LIMIT_PRO_SYNC: "25" # Pro tier sync API executions per minute
97+
RATE_LIMIT_PRO_ASYNC: "200" # Pro tier async API executions per minute
98+
RATE_LIMIT_TEAM_SYNC: "75" # Team tier sync API executions per minute
99+
RATE_LIMIT_TEAM_ASYNC: "500" # Team tier async API executions per minute
100+
RATE_LIMIT_ENTERPRISE_SYNC: "150" # Enterprise tier sync API executions per minute
101+
RATE_LIMIT_ENTERPRISE_ASYNC: "1000" # Enterprise tier async API executions per minute
90102

91103
# Service configuration
92104
service:

0 commit comments

Comments
 (0)