Default public API rate limit
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).
MCP server
Ordinary MCP data calls use the plan burst shared by an account’s OAuth tokens and dashboard-issuedwm_… keys. Data-free protocol methods use a separate fixed account bucket. Validated paid MCP panel reads use a separate panel bucket.
See MCP for details.
Per-plan API rate limits
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.
- Per-minute is a hard burst limit — exceeding it returns 429 immediately.
- Daily included is your plan’s allowance; it resets at 00:00 UTC. Requests beyond it are rejected with 429 — the sold plan limit is authoritative, with no overage headroom. Usage is metered against the same counter that enforces, so the Settings notice and the 429 agree.
- MCP calls draw on this same daily allowance. An API plan has one budget covering both REST requests and MCP tool calls; there is no separate MCP allowance to track. An MCP call that answers from cache costs the same as one REST request, one that fetches live data downstream costs two,
get_country_briefcosts three, andget_airspacecosts five. See MCP calls against an API plan. - The per-minute burst and daily allowance are both per account (shared across all of an account’s
wm_…keys), so issuing more keys does not raise your limit. (Operator-issued Enterprise keys are the exception — each is rate-limited independently, with its own daily ceilings of 100,000 REST requests, 10,000 MCP units and 5,000 direct-AI requests.) - Need a higher limit? Contact support to raise your plan’s allowance.
MCP calls against an API plan
API Starter and API Business do not carry a separate MCP allowance. Their MCP tool calls draw on the same daily budget as their REST requests, so 1,000 requests/day on API Starter is 1,000 units of work per UTC day however you spend it. A single MCP tool call is not always one unit, because the tools do not cost the same:get_airspace has a fixed weight of 5, including when only one source or longitude interval is needed. Dateline-crossing countries require two bounded queries per source. Dedicated Pro MCP allowances charge one unit per standalone call. Embedded panels include their internal readers as described below.
A weighted call is all-or-nothing. With 999 of 1,000 units spent, a 2-unit call is rejected rather than partly served. describe_tool and get_sources charge nothing, as do all the discovery methods.
You do not have to read this table to know what a call costs. Every tools/list and describe_tool entry carries its own price at _meta["worldmonitor/weight"], so a client can budget before it calls. The number is what the tool costs; whether it is charged is _meta["worldmonitor/access"]’s question, and only an API plan pays the weight — Pro and Pro Business pay one unit per standalone call or panel request.
Today the REST and MCP halves reserve against separate counters that each hold the plan’s full number, so an MCP-only workload and a REST-only workload both get the whole 1,000 or 10,000. They become one physical counter when REST enforcement turns on. Plan against the combined figure, because that is the allowance the plan sells and the merge will not raise it.
get_mcp_allowance({}) reads the current verified account counters without spending daily allowance, even at the daily cap. It uses the shared 192/minute protocol bucket and requires user-bound OAuth or wm_… credentials. The same snapshot is available at worldmonitor://account/mcp-allowance. Account selectors and panel_request are rejected. Unreadable counters return an error, never a zero snapshot. sharedWithRestApi states whether used also includes REST traffic; account totals do not identify individual callers.
Paid embedded-panel requests
Pro includes 50 requests per UTC day. Pro Business includes 250. Both use a dedicated MCP counter. Opening any embedded panel costs one request, including its rendered details. The ten single-result widgets already obtain their data in one tool call, and loading a visual shell costs nothing. The country brief and news/maps dashboard also include their internal data calls in that allocation. A country brief includes its section reads, default assessment and source coverage. A news dashboard includes its category panels and bounded hazard snapshots. A request includes up to 64 uncached reads. Panel openings retain the account’s plan burst limit. Internal reads with a valid paid panel admission use separate 64 uncached requests per minute and 64 cached replays per minute limits per admission. Only server-confirmed cached results use the replay bucket, so reopening a loaded result card does not spend the unfinished sections’ read burst. The admission still permits at most 64 uncached executions over its reuse window. Every read checks current account authorization, admission ownership, expiry, and allowed data scope. Invalid admissions and standalone calls retain the account burst limit. API plans retain their account burst limit for all calls. Same-country opens, default news-dashboard opens, curated market opens, and prediction opens share their respective admissions in a five-minute window. News display filters share the full loaded digest. Market filters reuse the market admission, including changes tosymbols, asset_class, and limit. These filters select the seeded snapshot and do not fetch arbitrary tickers or account watchlists. The admission lasts five to ten minutes, clipped at UTC midnight. Ready responses are reused while that admission is valid. Topic navigation and repeated questions in the loaded interface reuse ready data for five minutes. Failures are retried rather than cached as successful observations.
For get_market_data and get_prediction_markets, refresh: true with a new UUID request_id starts one new allocation. Retries with the same UUID reuse that allocation and its original expiry, including different letter case. Omit panel_request when refreshing. Ordinary curated reads can pass the returned panelRequest.token as panel_request until expiry. An API plan does not accept this receipt and retains per-tool billing.
Prediction filters reuse the prediction admission across category, source, query, and limit changes. Identical canonical filters replay successful original results. Each new uncached filter executes the fixed bootstrap and freshness reads. The 64-execution bound counts tool executions, not Redis commands. Paid prediction calls validate filters before reservation. API and free calls retain existing filter coercion and reject paid refresh controls. Fresh valid filtered-empty results can replay. Stale, malformed, unavailable, and unknown observations remain retryable without refunding the opening. An authorized explicit prediction receipt read also queries the current daily counter without a new allocation. An unavailable or unconfirmed counter omits the numeric notice while preserving loaded contracts.
Refresh country, Refresh news and Refresh map data each start one new request and reload that view’s observations. News refresh includes selected hazard layers. Layer toggles, country topic tabs and local news filters reuse loaded data. New AI assessment starts a new request for the assessment. Requested news summaries and translations are separate AI requests, each costing one unit. Other standalone data tools still cost one unit each.
Direct get_country_brief and get_country_risk calls share the existing same-country allocation, including analytical framework variants. Accepted originals replay per tool and effective analysis. Unknown, incomplete, retained or failed source results remain retryable. refresh: true requires a UUID request_id and no panel_request; a new UUID starts one country allocation and the same UUID retries it. This refresh rereads the sources but does not force brief generation or bypass its six-hour backend cache. The 64-read ceiling and ordinary API weights remain unchanged.
All twelve embedded views show remaining requests and the local reset time from their latest observed paid request. Single-result widgets receive usage metadata with their tool result; composite views receive it with their admission. Settings and the account-allowance resource read the same daily counter. Reuse cannot extend an admission past expiry or bypass a subscription denial. An admitted request remains charged after an upstream failure.
API Starter and API Business retain weighted per-tool billing. The panel admission does not exempt an API plan from its REST budget.
The 50 and 250 numerical limits remain the published paid allowances. Their unit covers a complete panel load, rather than each internal call. These numerical caps remain provisional pending provider costs and realistic customer usage. The workflow measurement report records read counts, reuse, work ceilings, and the limits of the cost evidence.
Dashboard AI quota
Dashboard and direct REST AI operations use a separate daily budget from MCP. The counter resets at 00:00 UTC.
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.
Stock backtest provider-work quota
GET /api/market/v1/backtest-stock is not LLM-backed. On a cache miss it fetches Yahoo Finance history for a caller-chosen symbol, so it must not share llm:direct-usage or dashboardAiCallsPerDay. Independently of the 60 requests / 60 s route policy:
The 200 ceiling is four full 50-symbol Pro watchlist hydrations. Cached repeats and invalid symbols do not consume the budget. Exceeding it returns 429 with
Retry-After until the next 00:00 UTC. If the quota store cannot prove a reservation, the route fails closed with 503 rather than fetching Yahoo.
OAuth endpoints
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.
All three grant types (authorization_code, refresh_token, client_credentials) fail open when the Upstash limiter is unconfigured or throws. Token persistence still fails closed when Redis storage is down; a limiter-only 503 would abort MCP client handshakes during an SDK timeout even when the pipeline path still works. client_credentials keeps the operator env-key allowlist as a second gate. Degradation is observable: a bounded/deduplicated [rate-limit] redis-error log and Sentry capture, X-RateLimit-Mode: degraded on the response (listed in Access-Control-Expose-Headers so cross-origin JS can read it), and a usage reason of rate_limit_degraded. Genuine budget exhaustion remains HTTP 429 rate_limit_exceeded.
Exceeding any of these during the OAuth flow will cause the MCP client to fail the connection handshake — wait 60 s and retry.
Provider proxies
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.
The two edge handlers (
/api/skills/fetch-agentskills, /api/youtube/live) enforce their budgets in-handler via checkScopedRateLimit/checkRateLimit; /api/reverse-geocode mirrors its per-IP budget as a literal constant per api/*.js constraints. /api/infrastructure/v1/reverse-geocode is a gateway RPC, so the gateway enforces its per-IP budget through checkEndpointRateLimit (fail-closed on Redis outage). After a shared-cache miss, both reverse-geocode handlers also use one fail-closed provider-wide Redis bucket capped at 1 request per second before they call Nominatim; cache hits do not consume that aggregate budget.
Uncached /api/symbol-search requests also require admission to one shared 30 requests/minute Finnhub bucket, independent of query and caller. Cached results do not spend this budget. Exhaustion returns 429 with Retry-After; unavailable admission storage returns 503 before Finnhub is called.
Aircraft identifier lookups consume the 30/minute budget before cache access, including cache hits and authenticated API or internal MCP requests. Accounts behind the same trusted client IP share this limit in addition to any account allowance; requests without a trusted IP share one fallback bucket. Bbox-only map requests keep the existing gateway policy. The native sidecar uses a separate 30/minute process-local window after local token authentication; restarting the process resets that window.
ICAO addresses must normalize to six lowercase hexadecimal characters. Callsigns must normalize to 1–8 uppercase alphanumeric characters; substring matching remains supported. Both raw inputs are capped at 16 characters before surrounding whitespace is removed. Invalid identifiers return 400 before cache or provider access. Quota exhaustion returns 429 with the 30-request policy and Retry-After; unavailable limiter storage returns 503 with X-RateLimit-Mode: degraded and Retry-After: 5.
Write endpoints
Other write endpoints (
/api/brief/share-url, /api/notification-channels, /api/create-checkout, etc.) fall back to the default per-IP limit above.
Bootstrap / health / version
These mostly use the default public API limit. Cache headers vary by endpoint:GET /api/bootstrap— only the explicitly-marked?...&public=1URLs are shared-cacheable.?tier=fast&public=1/?tier=slow&public=1use browsermax-age=60/max-age=300and CDNs-maxage=600/s-maxage=7200. Single-key public URLs: on-demand keys (?keys=<onDemandName>&public=1) inherit the slow profile — browsermax-age=300, CDNs-maxage=7200— unless the key declares its own, which every key published more often than that shield does:correlationCards(browsermax-age=60, CDNs-maxage=300),chinaDecisionSignals(browsermax-age=60, CDNs-maxage=900),canadaRoads(browsermax-age=60, CDNs-maxage=900),albertaRoads(browsermax-age=60, CDNs-maxage=900),manitobaRoads(browsermax-age=60, CDNs-maxage=900),marketCorrelationSeries(browsermax-age=60, CDNs-maxage=900),imdCycloneMarine(browsermax-age=60, CDNs-maxage=900),bcOpen511(browsermax-age=60, CDNs-maxage=1800),flightDelays(browsermax-age=60, CDNs-maxage=1800),liveVideoResolved(browsermax-age=300, CDNs-maxage=1800), andforecasts(browsermax-age=300, CDNs-maxage=3600);?keys=weatherAlerts&public=1usesCache-Control: public, s-maxage=600, stale-while-revalidate=120, stale-if-error=900with the fast-tier CDN shield. Every other shape — key-authenticated, session-authenticated, the unmarked?tier=...URLs, and the anonymous?keys=weatherAlertspath — usesCache-Control: no-storeand 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=0plusCDN-Cache-Control: no-store.GET /api/version—public, s-maxage=300, stale-while-revalidate=60, stale-if-error=3600.
Rate limit response headers (self-throttle before a 429)
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— the applicable quota (q) over a window ofwseconds 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.
X-RateLimit-* names are also emitted for back-compat.
Response when limited
An HTTP 429 additionally carries the live per-window counters (remaining is0; the reset and Retry-After are delta-seconds) plus the combined RateLimit member:
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 guidance
- Respect
Retry-After. Don’t pound on a 429. - For batch work, pace yourself: at 600 req/min/IP the default gives you ~10 req/s headroom.
- For MCP, the 60/min on Pro, Pro Business and API Starter is generous for conversational use but tight for scripted batch fetches. Prefer the REST API for batch, or API Business at 300/min.
- Spurious 429s often mean you’re sharing an egress IP (corporate proxy, CI runner). Contact support for a per-key limit bump if needed.
Customer notifications and paid-plan caps
API and MCP plan caps are tracked against the product catalog limits that ship with entitlements:
Pro and Pro Business have no REST allowance. Their separate counter charges one unit per standalone call or bounded panel request. API plans have one budget. The “MCP units / day” column is not an additional allowance on top of the API one, it is the same number, spent at the per-tool weight.
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.
Hard caps (not soft limits)
- Webhook callback URLs must be HTTPS (except localhost).
api/downloadfile sizes capped at ~50 MB per request.POST /api/scenario/v1/run-scenarioglobally pauses new jobs when the pending queue exceeds 100 — returns 429.api/v2/shipping/webhooksTTL is 30 days — re-register to extend.
