Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/sim/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions apps/sim/services/queue/RateLimiter.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => ({
Expand Down Expand Up @@ -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()
})
Expand Down Expand Up @@ -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)
})

Expand Down
42 changes: 26 additions & 16 deletions apps/sim/services/queue/RateLimiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -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),
}
}

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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),
}
}

Expand All @@ -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}`,
Expand Down Expand Up @@ -154,15 +164,15 @@ 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)
// Allow execution on error to avoid blocking users
return {
allowed: true,
remaining: 0,
resetAt: new Date(Date.now() + 60000),
resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS),
}
}
}
Expand All @@ -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),
}
}

Expand All @@ -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()
Expand All @@ -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),
}
}

Expand All @@ -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)
Expand All @@ -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),
}
}
}
Expand Down
23 changes: 15 additions & 8 deletions apps/sim/services/queue/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { InferSelectModel } from 'drizzle-orm'
import { env } from '@/lib/env'
import type { userRateLimits } from '@/db/schema'

// Database types
Expand All @@ -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<SubscriptionPlan, RateLimitConfig> = {
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,
},
}

Expand Down
12 changes: 12 additions & 0 deletions helm/sim/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down