Skip to content

Commit aea32d4

Browse files
feat(rate-limiter): token bucket algorithm (#2270)
* fix(ratelimit): make deployed chat rate limited * improvement(rate-limiter): use token bucket algo * update docs * fix * fix type * fix db rate limiter * address greptile comments
1 parent 22abf98 commit aea32d4

File tree

20 files changed

+8484
-631
lines changed

20 files changed

+8484
-631
lines changed

apps/docs/content/docs/en/execution/api.mdx

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@ All API responses include information about your workflow execution limits and u
2727
"limits": {
2828
"workflowExecutionRateLimit": {
2929
"sync": {
30-
"limit": 60, // Max sync workflow executions per minute
31-
"remaining": 58, // Remaining sync workflow executions
32-
"resetAt": "..." // When the window resets
30+
"requestsPerMinute": 60, // Sustained rate limit per minute
31+
"maxBurst": 120, // Maximum burst capacity
32+
"remaining": 118, // Current tokens available (up to maxBurst)
33+
"resetAt": "..." // When tokens next refill
3334
},
3435
"async": {
35-
"limit": 60, // Max async workflow executions per minute
36-
"remaining": 59, // Remaining async workflow executions
37-
"resetAt": "..." // When the window resets
36+
"requestsPerMinute": 200, // Sustained rate limit per minute
37+
"maxBurst": 400, // Maximum burst capacity
38+
"remaining": 398, // Current tokens available
39+
"resetAt": "..." // When tokens next refill
3840
}
3941
},
4042
"usage": {
@@ -46,7 +48,7 @@ All API responses include information about your workflow execution limits and u
4648
}
4749
```
4850

49-
**Note:** The rate limits in the response body are for workflow executions. The rate limits for calling this API endpoint are in the response headers (`X-RateLimit-*`).
51+
**Note:** Rate limits use a token bucket algorithm. `remaining` can exceed `requestsPerMinute` up to `maxBurst` when you haven't used your full allowance recently, allowing for burst traffic. The rate limits in the response body are for workflow executions. The rate limits for calling this API endpoint are in the response headers (`X-RateLimit-*`).
5052

5153
### Query Logs
5254

@@ -108,13 +110,15 @@ Query workflow execution logs with extensive filtering options.
108110
"limits": {
109111
"workflowExecutionRateLimit": {
110112
"sync": {
111-
"limit": 60,
112-
"remaining": 58,
113+
"requestsPerMinute": 60,
114+
"maxBurst": 120,
115+
"remaining": 118,
113116
"resetAt": "2025-01-01T12:35:56.789Z"
114117
},
115118
"async": {
116-
"limit": 60,
117-
"remaining": 59,
119+
"requestsPerMinute": 200,
120+
"maxBurst": 400,
121+
"remaining": 398,
118122
"resetAt": "2025-01-01T12:35:56.789Z"
119123
}
120124
},
@@ -184,13 +188,15 @@ Retrieve detailed information about a specific log entry.
184188
"limits": {
185189
"workflowExecutionRateLimit": {
186190
"sync": {
187-
"limit": 60,
188-
"remaining": 58,
191+
"requestsPerMinute": 60,
192+
"maxBurst": 120,
193+
"remaining": 118,
189194
"resetAt": "2025-01-01T12:35:56.789Z"
190195
},
191196
"async": {
192-
"limit": 60,
193-
"remaining": 59,
197+
"requestsPerMinute": 200,
198+
"maxBurst": 400,
199+
"remaining": 398,
194200
"resetAt": "2025-01-01T12:35:56.789Z"
195201
}
196202
},
@@ -467,17 +473,25 @@ Failed webhook deliveries are retried with exponential backoff and jitter:
467473

468474
## Rate Limiting
469475

470-
The API implements rate limiting to ensure fair usage:
476+
The API uses a **token bucket algorithm** for rate limiting, providing fair usage while allowing burst traffic:
471477

472-
- **Free plan**: 10 requests per minute
473-
- **Pro plan**: 30 requests per minute
474-
- **Team plan**: 60 requests per minute
475-
- **Enterprise plan**: Custom limits
478+
| Plan | Requests/Minute | Burst Capacity |
479+
|------|-----------------|----------------|
480+
| Free | 10 | 20 |
481+
| Pro | 30 | 60 |
482+
| Team | 60 | 120 |
483+
| Enterprise | 120 | 240 |
484+
485+
**How it works:**
486+
- Tokens refill at `requestsPerMinute` rate
487+
- You can accumulate up to `maxBurst` tokens when idle
488+
- Each request consumes 1 token
489+
- Burst capacity allows handling traffic spikes
476490

477491
Rate limit information is included in response headers:
478-
- `X-RateLimit-Limit`: Maximum requests per window
479-
- `X-RateLimit-Remaining`: Requests remaining in current window
480-
- `X-RateLimit-Reset`: ISO timestamp when the window resets
492+
- `X-RateLimit-Limit`: Requests per minute (refill rate)
493+
- `X-RateLimit-Remaining`: Current tokens available
494+
- `X-RateLimit-Reset`: ISO timestamp when tokens next refill
481495

482496
## Example: Polling for New Logs
483497

apps/docs/content/docs/en/execution/costs.mdx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,20 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
143143
{
144144
"success": true,
145145
"rateLimit": {
146-
"sync": { "isLimited": false, "limit": 10, "remaining": 10, "resetAt": "2025-09-08T22:51:55.999Z" },
147-
"async": { "isLimited": false, "limit": 50, "remaining": 50, "resetAt": "2025-09-08T22:51:56.155Z" },
146+
"sync": {
147+
"isLimited": false,
148+
"requestsPerMinute": 25,
149+
"maxBurst": 50,
150+
"remaining": 50,
151+
"resetAt": "2025-09-08T22:51:55.999Z"
152+
},
153+
"async": {
154+
"isLimited": false,
155+
"requestsPerMinute": 200,
156+
"maxBurst": 400,
157+
"remaining": 400,
158+
"resetAt": "2025-09-08T22:51:56.155Z"
159+
},
148160
"authType": "api"
149161
},
150162
"usage": {
@@ -155,6 +167,11 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
155167
}
156168
```
157169

170+
**Rate Limit Fields:**
171+
- `requestsPerMinute`: Sustained rate limit (tokens refill at this rate)
172+
- `maxBurst`: Maximum tokens you can accumulate (burst capacity)
173+
- `remaining`: Current tokens available (can be up to `maxBurst`)
174+
158175
**Response Fields:**
159176
- `currentPeriodCost` reflects usage in the current billing period
160177
- `limit` is derived from individual limits (Free/Pro) or pooled organization limits (Team/Enterprise)

apps/sim/app/api/chat/[identifier]/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ export async function POST(
151151
triggerType: 'chat',
152152
executionId,
153153
requestId,
154-
checkRateLimit: false, // Chat bypasses rate limits
155-
checkDeployment: true, // Chat requires deployed workflows
154+
checkRateLimit: true,
155+
checkDeployment: true,
156156
loggingSession,
157157
})
158158

apps/sim/app/api/users/me/usage-limits/route.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export async function GET(request: NextRequest) {
1818
}
1919
const authenticatedUserId = auth.userId
2020

21-
// Rate limit info (sync + async), mirroring /users/me/rate-limit
2221
const userSubscription = await getHighestPrioritySubscription(authenticatedUserId)
2322
const rateLimiter = new RateLimiter()
2423
const triggerType = auth.authType === 'api_key' ? 'api' : 'manual'
@@ -37,7 +36,6 @@ export async function GET(request: NextRequest) {
3736
),
3837
])
3938

40-
// Usage summary (current period cost + limit + plan)
4139
const [usageCheck, effectiveCost, storageUsage, storageLimit] = await Promise.all([
4240
checkServerSideUsageLimits(authenticatedUserId),
4341
getEffectiveCurrentPeriodCost(authenticatedUserId),
@@ -52,13 +50,15 @@ export async function GET(request: NextRequest) {
5250
rateLimit: {
5351
sync: {
5452
isLimited: syncStatus.remaining === 0,
55-
limit: syncStatus.limit,
53+
requestsPerMinute: syncStatus.requestsPerMinute,
54+
maxBurst: syncStatus.maxBurst,
5655
remaining: syncStatus.remaining,
5756
resetAt: syncStatus.resetAt,
5857
},
5958
async: {
6059
isLimited: asyncStatus.remaining === 0,
61-
limit: asyncStatus.limit,
60+
requestsPerMinute: asyncStatus.requestsPerMinute,
61+
maxBurst: asyncStatus.maxBurst,
6262
remaining: asyncStatus.remaining,
6363
resetAt: asyncStatus.resetAt,
6464
},

apps/sim/app/api/v1/logs/meta.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import { RateLimiter } from '@/lib/core/rate-limiter'
66
export interface UserLimits {
77
workflowExecutionRateLimit: {
88
sync: {
9-
limit: number
9+
requestsPerMinute: number
10+
maxBurst: number
1011
remaining: number
1112
resetAt: string
1213
}
1314
async: {
14-
limit: number
15+
requestsPerMinute: number
16+
maxBurst: number
1517
remaining: number
1618
resetAt: string
1719
}
@@ -40,12 +42,14 @@ export async function getUserLimits(userId: string): Promise<UserLimits> {
4042
return {
4143
workflowExecutionRateLimit: {
4244
sync: {
43-
limit: syncStatus.limit,
45+
requestsPerMinute: syncStatus.requestsPerMinute,
46+
maxBurst: syncStatus.maxBurst,
4447
remaining: syncStatus.remaining,
4548
resetAt: syncStatus.resetAt.toISOString(),
4649
},
4750
async: {
48-
limit: asyncStatus.limit,
51+
requestsPerMinute: asyncStatus.requestsPerMinute,
52+
maxBurst: asyncStatus.maxBurst,
4953
remaining: asyncStatus.remaining,
5054
resetAt: asyncStatus.resetAt.toISOString(),
5155
},

apps/sim/app/api/v1/middleware.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type NextRequest, NextResponse } from 'next/server'
22
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
3-
import { RateLimiter } from '@/lib/core/rate-limiter/rate-limiter'
3+
import { RateLimiter } from '@/lib/core/rate-limiter'
44
import { createLogger } from '@/lib/logs/console/logger'
55
import { authenticateV1Request } from '@/app/api/v1/auth'
66

@@ -12,6 +12,7 @@ export interface RateLimitResult {
1212
remaining: number
1313
resetAt: Date
1414
limit: number
15+
retryAfterMs?: number
1516
userId?: string
1617
error?: string
1718
}
@@ -26,7 +27,7 @@ export async function checkRateLimit(
2627
return {
2728
allowed: false,
2829
remaining: 0,
29-
limit: 10, // Default to free tier limit
30+
limit: 10,
3031
resetAt: new Date(),
3132
error: auth.error,
3233
}
@@ -35,12 +36,11 @@ export async function checkRateLimit(
3536
const userId = auth.userId!
3637
const subscription = await getHighestPrioritySubscription(userId)
3738

38-
// Use api-endpoint trigger type for external API rate limiting
3939
const result = await rateLimiter.checkRateLimitWithSubscription(
4040
userId,
4141
subscription,
4242
'api-endpoint',
43-
false // Not relevant for api-endpoint trigger type
43+
false
4444
)
4545

4646
if (!result.allowed) {
@@ -51,7 +51,6 @@ export async function checkRateLimit(
5151
})
5252
}
5353

54-
// Get the actual rate limit for this user's plan
5554
const rateLimitStatus = await rateLimiter.getRateLimitStatusWithSubscription(
5655
userId,
5756
subscription,
@@ -60,8 +59,11 @@ export async function checkRateLimit(
6059
)
6160

6261
return {
63-
...result,
64-
limit: rateLimitStatus.limit,
62+
allowed: result.allowed,
63+
remaining: result.remaining,
64+
resetAt: result.resetAt,
65+
limit: rateLimitStatus.requestsPerMinute,
66+
retryAfterMs: result.retryAfterMs,
6567
userId,
6668
}
6769
} catch (error) {
@@ -88,6 +90,10 @@ export function createRateLimitResponse(result: RateLimitResult): NextResponse {
8890
}
8991

9092
if (!result.allowed) {
93+
const retryAfterSeconds = result.retryAfterMs
94+
? Math.ceil(result.retryAfterMs / 1000)
95+
: Math.ceil((result.resetAt.getTime() - Date.now()) / 1000)
96+
9197
return NextResponse.json(
9298
{
9399
error: 'Rate limit exceeded',
@@ -98,7 +104,7 @@ export function createRateLimitResponse(result: RateLimitResult): NextResponse {
98104
status: 429,
99105
headers: {
100106
...headers,
101-
'Retry-After': Math.ceil((result.resetAt.getTime() - Date.now()) / 1000).toString(),
107+
'Retry-After': retryAfterSeconds.toString(),
102108
},
103109
}
104110
)

apps/sim/background/workspace-notification-delivery.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,14 @@ async function buildPayload(
116116

117117
payload.data.rateLimits = {
118118
sync: {
119-
limit: syncStatus.limit,
119+
requestsPerMinute: syncStatus.requestsPerMinute,
120+
maxBurst: syncStatus.maxBurst,
120121
remaining: syncStatus.remaining,
121122
resetAt: syncStatus.resetAt.toISOString(),
122123
},
123124
async: {
124-
limit: asyncStatus.limit,
125+
requestsPerMinute: asyncStatus.requestsPerMinute,
126+
maxBurst: asyncStatus.maxBurst,
125127
remaining: asyncStatus.remaining,
126128
resetAt: asyncStatus.resetAt.toISOString(),
127129
},
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
export { RateLimiter } from '@/lib/core/rate-limiter/rate-limiter'
2-
export type {
3-
RateLimitConfig,
4-
SubscriptionPlan,
5-
TriggerType,
6-
} from '@/lib/core/rate-limiter/types'
7-
export { RATE_LIMITS, RateLimitError } from '@/lib/core/rate-limiter/types'
1+
export type { RateLimitResult, RateLimitStatus } from './rate-limiter'
2+
export { RateLimiter } from './rate-limiter'
3+
export type { RateLimitStorageAdapter, TokenBucketConfig } from './storage'
4+
export type { RateLimitConfig, SubscriptionPlan, TriggerType } from './types'
5+
export { RATE_LIMITS, RateLimitError } from './types'

0 commit comments

Comments
 (0)