fix: increase global API rate limits to 1000/min

This commit is contained in:
Lucas Smith
2026-07-08 17:07:31 +10:00
parent 1b1e3d197b
commit c6b797a6ce
7 changed files with 24 additions and 14 deletions
@@ -11,9 +11,14 @@ Documenso enforces rate limits on all API endpoints to ensure service stability.
## HTTP Rate Limits
**Limit:** 100 requests per minute per IP address
**Limit:** 1000 requests per minute per IP address
**Response:** 429 Too Many Requests
<Callout type="info">
This is the global per-IP ceiling. Your organisation may have its own rate limits configured below
this value, in which case you can be rate-limited before reaching the global limit.
</Callout>
### Rate Limit Response
```json
@@ -472,7 +472,7 @@ Send the same document to multiple recipients in parallel. Useful for policy ack
<code>distributeDocument: true</code>
</Step>
<Step>
Process in batches with a short delay to respect rate limits (e.g. 100 requests/minute)
Process in batches with a short delay to respect rate limits (e.g. 1000 requests/minute)
</Step>
</Steps>
@@ -638,8 +638,8 @@ done
</Tabs>
<Callout type="info">
The API allows 100 requests per minute. For large batches, implement rate limiting with delays
between requests to avoid hitting limits.
The API allows 1000 requests per minute (your organisation may have its own lower limit). For large
batches, implement rate limiting with delays between requests to avoid hitting limits.
</Callout>
---
@@ -483,7 +483,7 @@ The API returns standard HTTP status codes and JSON error responses:
### Handling Rate Limits
The API allows 100 requests per minute per IP address. When rate limited, wait at least 60 seconds before retrying:
The API allows 1000 requests per minute per IP address. Your organisation may have its own lower rate limits. When rate limited, wait at least 60 seconds before retrying:
```javascript
async function fetchWithRetry(url, options, maxRetries = 3) {
+6 -1
View File
@@ -41,12 +41,17 @@ When a limit is reached, requests return a `429 Too Many Requests` response with
| Action | Limit | Window |
| --- | --- | --- |
| API requests (v1 and v2) | 100 requests | 1 minute |
| API requests (v1 and v2) | 1000 requests | 1 minute |
| File uploads | 20 requests | 1 minute |
| AI features | 3 requests | 1 minute |
Authentication endpoints (login, signup, password reset, etc.) are also rate-limited to protect against abuse.
<Callout type="info">
The API request limit above is the global per-IP ceiling. Individual organisations also have their
own rate limits, which may be configured below this value.
</Callout>
<Callout type="info">
Rate limits may vary by plan. Enterprise plans can include higher or custom limits. Contact
[sales](https://documen.so/sales) for details.
@@ -50,7 +50,7 @@ import type { Organisation, Team, User } from '@prisma/client';
*
* --- GLOBAL LIMIT AWARENESS ---
* apps/remix/server/router.ts applies a GLOBAL per-IP limiter to /api/v1/*:
* apiV1RateLimit = 100 requests / 1 minute (action `api.v1`, see rate-limits.ts).
* apiV1RateLimit = 1000 requests / 1 minute (action `api.v1`, see rate-limits.ts).
* Every per-org limit/quota configured here is kept FAR below that ceiling (single
* digits) and the suite runs serially so the shared-IP global bucket is never the
* thing that trips. A global-limit 429 is shaped `{ error }` whereas an org-limit
@@ -62,7 +62,7 @@ const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
const baseUrl = `${WEBAPP_BASE_URL}/api/v1`;
// Run serially: all workers share one IP, and the global /api/v1 limiter is
// per-IP. Serial execution keeps the shared global bucket well under 100/min.
// per-IP. Serial execution keeps the shared global bucket well under 1000/min.
test.describe.configure({ mode: 'serial' });
// This suite is only meaningful with real rate limiting enabled. CI sets the
@@ -125,7 +125,7 @@ const setClaimLimits = async (team: Team, limits: ClaimLimits) => {
* GLOBAL /api/v1 IP bucket so a fresh scenario starts from zero.
*
* - The org windowed limiter keys its rows `ip:org:<id>`.
* - The GLOBAL limiter (apps/remix/server/router.ts -> apiV1RateLimit, 100/min
* - The GLOBAL limiter (apps/remix/server/router.ts -> apiV1RateLimit, 1000/min
* per IP, action `api.v1`) is shared by EVERY v1 request from this test client.
* Across the suite (and especially across repeated local runs within the same
* minute) that shared bucket would otherwise fill up and trip BEFORE the org
@@ -37,7 +37,7 @@ import type { Organisation, Team, User } from '@prisma/client';
*
* --- GLOBAL LIMIT AWARENESS ---
* apps/remix/server/router.ts applies a GLOBAL per-IP limiter to /api/v2/*:
* apiV2RateLimit = 100 requests / 1 minute (see rate-limits.ts).
* apiV2RateLimit = 1000 requests / 1 minute (see rate-limits.ts).
* Every per-org limit/quota configured here is kept FAR below that ceiling (single
* digits) and the suite runs serially so the shared-IP global bucket is never the
* thing that trips. A global-limit 429 is shaped `{ error }` whereas an org-limit
@@ -49,7 +49,7 @@ const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
const baseUrl = `${WEBAPP_BASE_URL}/api/v2-beta`;
// Run serially: all workers share one IP, and the global /api/v2 limiter is
// per-IP. Serial execution keeps the shared global bucket well under 100/min.
// per-IP. Serial execution keeps the shared global bucket well under 1000/min.
test.describe.configure({ mode: 'serial' });
// This suite is only meaningful with real rate limiting enabled. CI sets the
@@ -84,19 +84,19 @@ export const syncSubscriptionRateLimit = createRateLimit({
export const apiV1RateLimit = createRateLimit({
action: 'api.v1',
max: 100,
max: 1000,
window: '1m',
});
export const apiV2RateLimit = createRateLimit({
action: 'api.v2',
max: 100,
max: 1000,
window: '1m',
});
export const apiTrpcRateLimit = createRateLimit({
action: 'api.trpc',
max: 100,
max: 1000,
window: '1m',
});