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
60 changes: 37 additions & 23 deletions apps/docs/content/docs/en/execution/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ All API responses include information about your workflow execution limits and u
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60, // Max sync workflow executions per minute
"remaining": 58, // Remaining sync workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 60, // Sustained rate limit per minute
"maxBurst": 120, // Maximum burst capacity
"remaining": 118, // Current tokens available (up to maxBurst)
"resetAt": "..." // When tokens next refill
},
"async": {
"limit": 60, // Max async workflow executions per minute
"remaining": 59, // Remaining async workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 200, // Sustained rate limit per minute
"maxBurst": 400, // Maximum burst capacity
"remaining": 398, // Current tokens available
"resetAt": "..." // When tokens next refill
}
},
"usage": {
Expand All @@ -46,7 +48,7 @@ All API responses include information about your workflow execution limits and u
}
```

**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-*`).
**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-*`).

### Query Logs

Expand Down Expand Up @@ -108,13 +110,15 @@ Query workflow execution logs with extensive filtering options.
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
Expand Down Expand Up @@ -184,13 +188,15 @@ Retrieve detailed information about a specific log entry.
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
Expand Down Expand Up @@ -467,17 +473,25 @@ Failed webhook deliveries are retried with exponential backoff and jitter:

## Rate Limiting

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

- **Free plan**: 10 requests per minute
- **Pro plan**: 30 requests per minute
- **Team plan**: 60 requests per minute
- **Enterprise plan**: Custom limits
| Plan | Requests/Minute | Burst Capacity |
|------|-----------------|----------------|
| Free | 10 | 20 |
| Pro | 30 | 60 |
| Team | 60 | 120 |
| Enterprise | 120 | 240 |

**How it works:**
- Tokens refill at `requestsPerMinute` rate
- You can accumulate up to `maxBurst` tokens when idle
- Each request consumes 1 token
- Burst capacity allows handling traffic spikes

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

## Example: Polling for New Logs

Expand Down
21 changes: 19 additions & 2 deletions apps/docs/content/docs/en/execution/costs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,20 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
{
"success": true,
"rateLimit": {
"sync": { "isLimited": false, "limit": 10, "remaining": 10, "resetAt": "2025-09-08T22:51:55.999Z" },
"async": { "isLimited": false, "limit": 50, "remaining": 50, "resetAt": "2025-09-08T22:51:56.155Z" },
"sync": {
"isLimited": false,
"requestsPerMinute": 25,
"maxBurst": 50,
"remaining": 50,
"resetAt": "2025-09-08T22:51:55.999Z"
},
"async": {
"isLimited": false,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 400,
"resetAt": "2025-09-08T22:51:56.155Z"
},
"authType": "api"
},
"usage": {
Expand All @@ -155,6 +167,11 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
}
```

**Rate Limit Fields:**
- `requestsPerMinute`: Sustained rate limit (tokens refill at this rate)
- `maxBurst`: Maximum tokens you can accumulate (burst capacity)
- `remaining`: Current tokens available (can be up to `maxBurst`)

**Response Fields:**
- `currentPeriodCost` reflects usage in the current billing period
- `limit` is derived from individual limits (Free/Pro) or pooled organization limits (Team/Enterprise)
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/chat/[identifier]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ export async function POST(
triggerType: 'chat',
executionId,
requestId,
checkRateLimit: false, // Chat bypasses rate limits
checkDeployment: true, // Chat requires deployed workflows
checkRateLimit: true,
checkDeployment: true,
loggingSession,
})

Expand Down
8 changes: 4 additions & 4 deletions apps/sim/app/api/users/me/usage-limits/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export async function GET(request: NextRequest) {
}
const authenticatedUserId = auth.userId

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

// Usage summary (current period cost + limit + plan)
const [usageCheck, effectiveCost, storageUsage, storageLimit] = await Promise.all([
checkServerSideUsageLimits(authenticatedUserId),
getEffectiveCurrentPeriodCost(authenticatedUserId),
Expand All @@ -52,13 +50,15 @@ export async function GET(request: NextRequest) {
rateLimit: {
sync: {
isLimited: syncStatus.remaining === 0,
limit: syncStatus.limit,
requestsPerMinute: syncStatus.requestsPerMinute,
maxBurst: syncStatus.maxBurst,
remaining: syncStatus.remaining,
resetAt: syncStatus.resetAt,
},
async: {
isLimited: asyncStatus.remaining === 0,
limit: asyncStatus.limit,
requestsPerMinute: asyncStatus.requestsPerMinute,
maxBurst: asyncStatus.maxBurst,
remaining: asyncStatus.remaining,
resetAt: asyncStatus.resetAt,
},
Expand Down
12 changes: 8 additions & 4 deletions apps/sim/app/api/v1/logs/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { RateLimiter } from '@/lib/core/rate-limiter'
export interface UserLimits {
workflowExecutionRateLimit: {
sync: {
limit: number
requestsPerMinute: number
maxBurst: number
remaining: number
resetAt: string
}
async: {
limit: number
requestsPerMinute: number
maxBurst: number
remaining: number
resetAt: string
}
Expand Down Expand Up @@ -40,12 +42,14 @@ export async function getUserLimits(userId: string): Promise<UserLimits> {
return {
workflowExecutionRateLimit: {
sync: {
limit: syncStatus.limit,
requestsPerMinute: syncStatus.requestsPerMinute,
maxBurst: syncStatus.maxBurst,
remaining: syncStatus.remaining,
resetAt: syncStatus.resetAt.toISOString(),
},
async: {
limit: asyncStatus.limit,
requestsPerMinute: asyncStatus.requestsPerMinute,
maxBurst: asyncStatus.maxBurst,
remaining: asyncStatus.remaining,
resetAt: asyncStatus.resetAt.toISOString(),
},
Expand Down
22 changes: 14 additions & 8 deletions apps/sim/app/api/v1/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
import { RateLimiter } from '@/lib/core/rate-limiter/rate-limiter'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { createLogger } from '@/lib/logs/console/logger'
import { authenticateV1Request } from '@/app/api/v1/auth'

Expand All @@ -12,6 +12,7 @@ export interface RateLimitResult {
remaining: number
resetAt: Date
limit: number
retryAfterMs?: number
userId?: string
error?: string
}
Expand All @@ -26,7 +27,7 @@ export async function checkRateLimit(
return {
allowed: false,
remaining: 0,
limit: 10, // Default to free tier limit
limit: 10,
resetAt: new Date(),
error: auth.error,
}
Expand All @@ -35,12 +36,11 @@ export async function checkRateLimit(
const userId = auth.userId!
const subscription = await getHighestPrioritySubscription(userId)

// Use api-endpoint trigger type for external API rate limiting
const result = await rateLimiter.checkRateLimitWithSubscription(
userId,
subscription,
'api-endpoint',
false // Not relevant for api-endpoint trigger type
false
)

if (!result.allowed) {
Expand All @@ -51,7 +51,6 @@ export async function checkRateLimit(
})
}

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

return {
...result,
limit: rateLimitStatus.limit,
allowed: result.allowed,
remaining: result.remaining,
resetAt: result.resetAt,
limit: rateLimitStatus.requestsPerMinute,
retryAfterMs: result.retryAfterMs,
userId,
}
} catch (error) {
Expand All @@ -88,6 +90,10 @@ export function createRateLimitResponse(result: RateLimitResult): NextResponse {
}

if (!result.allowed) {
const retryAfterSeconds = result.retryAfterMs
? Math.ceil(result.retryAfterMs / 1000)
: Math.ceil((result.resetAt.getTime() - Date.now()) / 1000)

return NextResponse.json(
{
error: 'Rate limit exceeded',
Expand All @@ -98,7 +104,7 @@ export function createRateLimitResponse(result: RateLimitResult): NextResponse {
status: 429,
headers: {
...headers,
'Retry-After': Math.ceil((result.resetAt.getTime() - Date.now()) / 1000).toString(),
'Retry-After': retryAfterSeconds.toString(),
},
}
)
Expand Down
6 changes: 4 additions & 2 deletions apps/sim/background/workspace-notification-delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,14 @@ async function buildPayload(

payload.data.rateLimits = {
sync: {
limit: syncStatus.limit,
requestsPerMinute: syncStatus.requestsPerMinute,
maxBurst: syncStatus.maxBurst,
remaining: syncStatus.remaining,
resetAt: syncStatus.resetAt.toISOString(),
},
async: {
limit: asyncStatus.limit,
requestsPerMinute: asyncStatus.requestsPerMinute,
maxBurst: asyncStatus.maxBurst,
remaining: asyncStatus.remaining,
resetAt: asyncStatus.resetAt.toISOString(),
},
Expand Down
12 changes: 5 additions & 7 deletions apps/sim/lib/core/rate-limiter/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export { RateLimiter } from '@/lib/core/rate-limiter/rate-limiter'
export type {
RateLimitConfig,
SubscriptionPlan,
TriggerType,
} from '@/lib/core/rate-limiter/types'
export { RATE_LIMITS, RateLimitError } from '@/lib/core/rate-limiter/types'
export type { RateLimitResult, RateLimitStatus } from './rate-limiter'
export { RateLimiter } from './rate-limiter'
export type { RateLimitStorageAdapter, TokenBucketConfig } from './storage'
export type { RateLimitConfig, SubscriptionPlan, TriggerType } from './types'
export { RATE_LIMITS, RateLimitError } from './types'
Loading