Rate limits
Limits by endpoint type, response headers, and why they are off by default in this environment.
Rate limiting is controlled by a single flag, RATE_LIMIT_ENABLED, and it is
off in local and kind development by default. Every request succeeds
regardless of volume in that environment. Where it matters, note which
environment a limit was observed in.
Limits by endpoint type
Requests are classified by the route they hit, not given one blanket limit:
| Type | Base limit | Scaled by plan | Keyed by |
|---|---|---|---|
auth | 10 / min | No | Caller IP |
api | 100 / min | Yes | Organization id |
user | 100 / min (20 / min if unauthenticated) | No | User subject, or IP if unauthenticated |
webhook | 60 / min | No | Source IP |
public | 60 / min | No | Caller IP |
Only api scales with plan tier, deliberately: scaling an authentication
endpoint's tolerance by plan would make credential stuffing easier the more a
customer pays, which is backwards. The multipliers, verified against
rate_limit_policy.py:
| Plan | Multiplier | Effective api limit |
|---|---|---|
| Free | 1x | 100 / min |
| Starter | 2x | 200 / min |
| Pro | 5x | 500 / min |
| Business | 10x | 1000 / min |
| Enterprise | 50x | 5000 / min |
These numbers differ from an older plan-limits table that predates the
endpoint-type scheme above; the table here is what the running middleware
does. An api-classified limit also applies to the whole
organization, not per member: every teammate's calls share one budget.
Headers
Every response, successful or not, carries:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1705577400A 429 additionally carries Retry-After (seconds), and its body is not the
usual error envelope. See Errors for the exact shape.
Enabling it locally
Set RATE_LIMIT_ENABLED=true in backend/.env and restart the backend to
observe real 429 responses while developing against it.