On this page
Rate Limits & Quotas
Artery enforces two layered budgets:
- Tier quota — total REST calls per key per month
(
apps/api/src/auth/api-key.types.ts → TIER_LIMITS). - Per-SKU quota — per-minute and per-day caps on specific usage
types (
api.request,ws.message,webhook.delivery,backtest.compute_min, …), resolved from the org's billing plan (apps/api/migrations/003_multitenancy.sql → plans).
A request must pass both layers. Either bucket can 429.
Per-key tier (monthly REST)
Set on key creation; default is free. From
TIER_LIMITS in source:
| Tier | REST calls / month | Concurrent WS | Notes |
|---|---|---|---|
free | 10,000 | 1 | Sandbox-friendly; no backtest |
builder | 500,000 | 5 | Auth tier for paid prototypes |
pro | 5,000,000 | 50 | Standard production |
enterprise | unlimited | unlimited | Headers report unlimited instead of a numeric limit |
bash# Pick the tier at key creation
curl -X POST "https://api.artery.questflow.ai/keys" \
-H "Content-Type: application/json" \
-d '{"name":"prod-bot","userId":"u-1","scopes":["read","stream"],"rateLimitTier":"pro"}'Per-SKU plan quotas
Plan-level quotas (from the plans table seeded by
003_multitenancy.sql). -1 means unlimited. The builder plan is
intentionally absent at the plan layer — it's a tier alias that maps to
free for SKU purposes until M3-6.
| Plan | api.request /min | api.request /day | ws.message /day | webhook.delivery /day |
|---|---|---|---|---|
free | 60 | 10,000 | 50,000 | 1,000 |
pro | 600 | 1,000,000 | 10,000,000 | 100,000 |
enterprise | 6,000 | unlimited | unlimited | unlimited |
The api.request SKU covers every authenticated REST handler. Per-key
overrides live in ApiKey.quotaOverrides and merge on top of the plan.
Plans also gate features (backtest, webhook.signing, history.maxDays, sso). Backtest
endpoints /v1/me/backtest/replay-arbitrage and /v1/me/backtest/replay-negation return 403 forbidden_scope on free plans even when the request quota would otherwise allow them.
Backtest costs
Replay endpoints consume the backtest.compute_min SKU in addition to
api.request. Plan limits aren't yet seeded for this SKU — current
behavior is unlimited for pro / enterprise and disabled for free.
A pro-tier 7-day arbitrage replay typically costs 4–8 compute-minutes.
Window constraints (enforced at the controller, not via quota):
| Constraint | Value |
|---|---|
to - from | ≤ 30 days |
fill_window_ms | ≤ 86,400,000 (24h) |
notional | [0.01, 1_000_000] |
min_net_edge | [0, 1] |
Response headers
Every successful authenticated REST response carries the tier-bucket state:
X-RateLimit-Limit: 500000
X-RateLimit-Remaining: 499234
X-RateLimit-Reset: 1780822653
X-RateLimit-Tier: builder
X-RateLimit-Reset is a Unix timestamp marking the next quota reset
(the start of next month UTC for monthly quotas).
For enterprise keys the headers report unlimited instead of a
numeric limit.
429 handling
When you hit the tier or SKU limit:
httpHTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Reset: 1780822653
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded for tier \"free\" (10000/month)",
"requestId": "req_..."
}
}Retry-After is in seconds. Most HTTP clients honor it automatically (e.g. axios-retry,
Python requests with urllib3.Retry). Don't sleep Retry-After * 1000 ms.
The plan-layer 429 differs only in the message:
"message": "Quota exceeded: api.request (perMinute 60)"
Parse error.message to distinguish — both share code: rate_limited.
WebSocket subscription limits
The /v1/stream gateway tracks subscriptions per connection.
| Bound | Value |
|---|---|
| Active subs / connection | 1,000 |
| Concurrent WS / key | per tier table above (free: 1 ... pro: 50) |
| Send-queue buffered bytes | 5 MiB — slow consumers are closed |
| Heartbeat idle timeout | 60s (server pushes {"type":"pong"} every 30s) |
Hitting the per-connection cap:
jsonc{ "type": "error", "error": "max 1000 subs/conn" }Hitting the per-key concurrent-WS cap returns close code 1008 with a
policy_violation reason during upgrade.
Upstream provider limits
Independent of Artery's quota. These surface as
code: upstream_rate_limited:
| Provider | Limit | Notes |
|---|---|---|
| Polymarket | ~600 req/min/IP | Cloudflare-level; aggressive |
| Kalshi | Token-bucket; 10 tokens/req | No Retry-After — Artery backs off automatically |
| Hyperliquid | 1200 req/min/IP for /info; 100 orders/sec/wallet | Trade-side is per master wallet |
Artery retries upstream_rate_limited on GET calls automatically —
see error handling.
See also
- Error code reference — 429 envelope details
- Rate limits concept — burst limits (planned)
- Authentication — scope + tier configuration