docs/usage-rate-limits.mdx
Rate limits are enforced at the Vercel Edge runtime using Upstash Redis counters. All limits are sliding 60-second windows unless noted.
| Scope | Limit | Window |
|---|---|---|
| Per IP (default) | 600 requests | 60 s |
Applies to all /api/* routes that don't have a stricter override. Implemented by api/_rate-limit.js (legacy api/*.js edge functions) and server/_shared/rate-limit.ts (the gateway and .ts edge functions).
| Scope | Limit | Window |
|---|---|---|
| Per API key (MCP tools) | 60 requests | 60 s |
See MCP for details.
Authenticated REST API keys (wm_…) are limited per account, not per IP — a key behind a shared egress IP is not throttled by other tenants' traffic, and all of an account's keys share one allowance.
| Plan | Per-minute (burst) | Daily included | Beyond the daily allowance |
|---|---|---|---|
| API Starter | 60 / 60 s | 1,000 / UTC day | metered — hard stop at 10,000 / day |
| API Business | 300 / 60 s | 10,000 / UTC day | metered — hard stop at 100,000 / day |
| Enterprise | 1,000 / 60 s | unlimited | — |
wm_… keys), so issuing more keys does not raise your limit. (Operator-issued Enterprise keys are the exception — each is rate-limited independently.)Dashboard and direct REST AI operations use a separate daily budget from MCP. The counter resets at 00:00 UTC.
| Plan | Dashboard-AI requests/day |
|---|---|
| Free / signed out | 0 — protected AI routes require Pro authentication |
| Pro | 500 |
| Pro Business | 2,500 |
| API Starter | 1,000 |
| API Business | 10,000 |
| Enterprise | unlimited |
Free and signed-out dashboard users keep the normal keyword/cache fallback for feed enrichment; they do not consume the paid direct-AI budget. These limits are independent of the MCP allowances above.
Signed-out callers are rejected outright, and Pro-gated AI routes deny free accounts before any spend. Separately from these plan allowances, a small non-plan safety floor of 50 requests/day applies to any caller whose paid entitlement cannot be confirmed at request time — a lapsed subscription, or a transient entitlement-lookup outage. It exists so an outage degrades gracefully instead of rejecting paying customers; it is not an allowance any plan includes, and it is never larger than the smallest paid allowance.
| Endpoint | Limit | Window | Scope |
|---|---|---|---|
POST /api/oauth/register | 5 | 60 s | Per IP |
GET /api/oauth/authorize | 10 | 60 s | Per IP |
POST /api/oauth/token | 10 | 60 s | Per credential / client / IP fallback |
Matches the implementations in api/oauth/register.js, api/oauth/authorize.js, and api/oauth/token.ts.
For /api/oauth/token, the limiter key is client_secret hash for client_credentials, then client_id when present, and only falls back to caller IP when neither credential identifier is available.
Exceeding any of these during the OAuth flow will cause the MCP client to fail the connection handshake — wait 60 s and retry.
Routes that fetch a third-party host on our behalf carry their own per-IP budget, so a single scripted caller cannot drive unbounded traffic to a provider we do not control. These budgets are per IP, not aggregate: they bound any one caller, but they do not cap total egress across all callers.
| Endpoint | Limit | Window | Scope |
|---|---|---|---|
POST /api/skills/fetch-agentskills | 30 | 60 s | Per IP |
GET /api/youtube/live | 30 | 60 s | Per IP |
GET /api/reverse-geocode | 60 | 60 s | Per IP |
GET /api/infrastructure/v1/reverse-geocode | 60 | 60 s | Per IP |
The two edge handlers (/api/skills/fetch-agentskills, /api/youtube/live) enforce their budgets in-handler via checkScopedRateLimit/checkRateLimit; /api/reverse-geocode mirrors its budget as a literal constant per api/*.js constraints. /api/infrastructure/v1/reverse-geocode is a gateway RPC, so the gateway enforces it via checkEndpointRateLimit (fail-closed on Redis outage). Both reverse-geocode routes reach Nominatim, whose usage policy is the strictest of any provider in the stack and whose enforcement is an egress-IP ban — treat those budgets as a ceiling rather than a target; a global (aggregate) companion budget is still required and is not implemented by this change.
| Endpoint | Limit | Window | Scope |
|---|---|---|---|
POST /api/scenario/v1/run-scenario | 10 | 60 s | Per IP |
POST /api/scenario/v1/run-scenario (queue depth) | 100 in-flight | — | Global |
POST /api/leads/v1/register-interest | 5 | 60 min | Per IP + Turnstile (desktop sources require signed HMAC bypass) |
POST /api/leads/v1/submit-contact | 3 | 60 min | Per IP + Turnstile |
Other write endpoints (/api/brief/share-url, /api/notification-channels, /api/create-checkout, /api/customer-portal, etc.) fall back to the default per-IP limit above.
These mostly use the default public API limit. Cache headers vary by endpoint:
GET /api/bootstrap — only the explicitly-marked ?...&public=1 URLs are shared-cacheable. ?tier=fast&public=1 / ?tier=slow&public=1 use browser max-age=60 / max-age=300 and CDN s-maxage=600 / s-maxage=7200. Single-key public URLs: on-demand keys (?keys=<onDemandName>&public=1) inherit the slow profile — browser max-age=300, CDN s-maxage=7200 — unless the key declares its own, which today only chinaDecisionSignals does (browser max-age=60, CDN s-maxage=900); ?keys=weatherAlerts&public=1 uses Cache-Control: public, s-maxage=600, stale-while-revalidate=120, stale-if-error=900 with the fast-tier CDN shield. Every other shape — key-authenticated, session-authenticated, the unmarked ?tier=... URLs, and the anonymous ?keys=weatherAlerts path — uses Cache-Control: no-store and emits no CDN cache headers, so a credentialed URL can never be answered from a shared cache. User API key validation also has a fail-closed fixed 60 s per-IP pre-validation limit of 600 attempts.GET /api/health — private, no-store, max-age=0 plus CDN-Cache-Control: no-store.GET /api/version — public, s-maxage=300, stale-while-revalidate=60, stale-if-error=3600.Every /api/* response — success or error — advertises the IETF RateLimit header fields so an agent can pace itself before it trips a 429:
RateLimit-Policy: "default";q=600;w=60
RateLimit-Limit: 600
RateLimit-Policy — the applicable quota (q) over a window of w seconds for the default sliding window. Stricter per-endpoint, per-plan, and OAuth limits (see the tables above) apply on those routes.RateLimit-Limit — the same quota as a bare integer, for parsers that predate the structured-field draft.These are static advertisements, so they add no latency on the hot path. The legacy X-RateLimit-* names are also emitted for back-compat.
An HTTP 429 additionally carries the live per-window counters (remaining is 0; the reset and Retry-After are delta-seconds) plus the combined RateLimit member:
HTTP/1.1 429 Too Many Requests
RateLimit-Policy: "default";q=<limit>;w=<window>
RateLimit-Limit: <limit>
RateLimit-Remaining: 0
RateLimit-Reset: <seconds until reset>
RateLimit: "default";r=0;t=<seconds until reset>
Retry-After: <seconds>
X-RateLimit-Limit: <limit>
X-RateLimit-Remaining: 0
X-RateLimit-Reset: <reset, ms since epoch>
Content-Type: application/json
{ "error": "Too many requests" }
Note the IETF RateLimit-Reset (and the t value in the combined RateLimit member) is seconds remaining, whereas the legacy X-RateLimit-Reset is an absolute epoch in milliseconds. For a daily-ceiling 429 the Retry-After counts down to the next 00:00 UTC.
Retry-After. Don't pound on a 429.API and MCP plan caps are tracked against the product catalog limits that ship with entitlements:
| Plan | API requests / day | API burst / minute | MCP calls / day | MCP burst / minute |
|---|---|---|---|---|
| Free | 0 | 0 | 0 | 0 |
| Pro | 0 | 0 | 50 | 60 |
| API Starter | 1,000 | 60 | 1,000 | 60 |
| API Business | 10,000 | 300 | 10,000 | 300 |
| Enterprise | Unlimited | 1,000 | Unlimited | 1,000 |
When a paid user approaches or exceeds one of these limits, WorldMonitor records a compact Convex rollup and opens a current account notice in Settings. The daily count is read from the same per-account meter that governs enforcement, so a warning reflects the same usage number the plan is metered against. Daily limits warn at 80% and switch to over-limit at 100%; burst limits only notify on sustained pressure, not a single isolated spike.
If the notice is still current, a Resend-backed lifecycle sends an email at a bounded cadence. The email and dashboard notice explain the current usage, the relevant plan limit, and the available options: reduce traffic, wait for reset, upgrade when a self-serve path exists, or contact support when the next tier is not self-serve.
WorldMonitor does not automatically upgrade a user, charge for overages, or move a customer into API Business because they crossed a cap. Any future hard enforcement for paid plans must first pass the internal apiPlanLimitNotices.getEnforcementReadiness gate: no stale usage source, no pending/failed email, and no blocked self-serve upgrade path.
api/download file sizes capped at ~50 MB per request.POST /api/scenario/v1/run-scenario globally pauses new jobs when the pending queue exceeds 100 — returns 429.api/v2/shipping/webhooks TTL is 30 days — re-register to extend.