Rate Limits
Rate limits are enforced per API key using fixed-window counters with both per-second (RPS) and per-minute (RPM) buckets. Each bucket is keyed on the current clock second and clock minute, and resets on the tick.Tiers
The
free tier allows up to 2 requests in any one clock second, and is
also capped at 120 requests per clock minute, so the per-minute bucket is
the one you’ll hit first under sustained load. Use POST /v1/prices/batch and the WebSocket feeds
(which don’t count against the REST limit) to stay well inside it.Trade Write Serialization vs. Rate Limits: Rate limits apply per API key to general REST requests. In addition, order-writing endpoints (
POST /v1/orders, POST /v1/orders/batch, DELETE /v1/orders/{id}, etc.) are serialized per user via an internal lock to prevent balance and position races. A second concurrent write for the same user waits cooperatively up to 2 seconds; if the first write is still in flight when the wait window expires, it returns 409 TRADE_IN_PROGRESS with Retry-After: 1. Using multiple API keys for the same account does not bypass this lock. Callers should honor Retry-After and retry with the same client_order_id. See Placing Orders and Error Handling.Free-tier daily order cap
On top of the per-key rate limits above, afree key can place up to
100 accepted orders per UTC calendar day. This is a placement cap, not a
request cap — reads, cancels and rejected orders do not count — and it is the
one limit on this page that is counted per user, not per key.
The cap is per user deliberately. Keys are free, so a per-key cap would be
defeated by minting hundreds of keys and rotating through them. The rate
limit bounds how fast one key can act; the daily cap bounds how much free
trading one person can do.
429 before
validating the order, so a spent quota surfaces as itself rather than as a
400 about a field you would fix and resend for nothing:
Retry-After is the whole number of seconds until the next UTC midnight
(never below 1), which is the machine-readable form of the reset. Send
X-Polysim-Verbose: true and the body carries
"details": {"limit": 100, "used": 100, "resets_at": "2026-09-19T00:00:00Z", "upgrade_url": "/pricing", "next_tier": "pro"}
in place of the top-level upgrade_url and next_tier.
Inside POST /v1/orders/batch a spent quota surfaces per entry as
"status": "REJECTED" with "error": "FREE_TIER_DAILY_LIMIT", and the batch
call itself still returns 200.
Rate Limit Response
When you exceed your limit, the API returns HTTP 429 with the standard two-field error envelope and a stableX-Polysim-Code
response header:
error or the identical X-Polysim-Code value, not on
message, and read the Retry-After header for the exact wait time
in seconds.
Rate Limit Headers (on authenticated responses)
Every authenticated response carriesx-ratelimit-* headers so bots
can pre-throttle instead of waiting for an actual 429 (unauthenticated
public routes and legacy keyed public 429s return Retry-After, X-Polysim-Code,
and X-Request-Id; full quota metadata headers are returned on authenticated
endpoints and flagged token resolver routes):
Every header above is also emitted under the PolySim-namespaced
x-polysim-ratelimit-* prefix with identical values (e.g.
x-polysim-ratelimit-remaining). Read whichever your SDK or proxy
keys off — the unprefixed x-ratelimit-* form is canonical.Handling Rate Limits
When the limiter does fire, wait at leastRetry-After seconds, add
jitter, and stop after a bounded number of attempts:
Two 429s look similar and mean different things.
RATE_LIMIT_EXCEEDED
clears within seconds. MONTHLY_QUOTA_EXCEEDED and the free-tier daily
order cap clear at the next UTC period, and their Retry-After says so.
Branch on error. Don’t retry a quota 429 with short backoff.Safe Order Polling Contract
A common pattern among trading bots is checking whether a resting limit order has been filled or canceled. Do not pollGET /v1/orders/{id} or GET /v1/orders in an unthrottled loop. Rapid polling exhausts per-second (RPS) and per-minute (RPM) buckets, generating 429 RATE_LIMIT_EXCEEDED responses.
The budget is per key, across every endpoint
Both buckets count every request the key makes, whatever the endpoint. Onfree (2 per second, 120 per minute), a bot that checks
GET /v1/orders, GET /v1/data/orders and GET /v1/orders/{id} together
every 2–3 seconds sends 3 requests in the same second. That is over the
2-per-second cap on every cycle, even though its per-minute rate (60–90)
is inside 120. Add backoff that ignores Retry-After and the bot spends the
rest of its budget on 429s.
The fix is fewer requests, not faster retries:
- One call per cycle, not three.
GET /v1/orders?status=PENDINGreturns your open orders 50 to a page. Below 50 open orders that is one request. Above it, follownext_cursorwhilehas_moreis true, and count those pages in your budget. Only an order missing from the complete list has filled or been cancelled. Look that order up once. - Spread requests out. Never send more than your tier’s per-second cap in one second. Read
x-ratelimit-remaining-per-secondand wait a beat when it reaches 0. - Let the execution feed tell you about fills (below), and poll REST only to reconcile.
Authoritative REST Polling Rules
REST endpoints are the authoritative source for order state, fills, and cancellations. When implementing order tracking:- Minimum 1–2 Second Interval: Never poll an individual resting order more frequently than once every 1–2 seconds. On
free, that interval covers all of your polling combined, not each endpoint separately. - Add Jitter: Apply randomized delays (e.g.
1.0s + random(0.1s, 0.5s)) to prevent synchronized polling bursts across multiple orders. - Honor
Retry-After: If a429occurs, parse theRetry-Afterheader and back off for at least that long before retrying. Keep backing off while 429s continue (see Handling Rate Limits). - Batch Fetching: Use
GET /v1/data/ordersorGET /v1/orders?status=PENDINGto inspect all open orders in a single request rather than executing individualGET /v1/orders/{id}calls per order.
Use the execution feed for fills, REST to reconcile
Use the WebSocket feeds to learn about fills as they happen. WebSocket traffic does not count against the REST limits:- PolySim Native:
WS /v1/ws/executions?token=<jwt>pushes afillframe for each limit-order fill. That includes fills made by the background matching engine, not only fills in the process your socket is connected to. - Polymarket Parity:
WS /v1/ws/user(withauth.apiKey) streams livetradeframes withstatus: "MATCHED".
/v1/ws/executions needs REST only to reconcile: one GET /v1/orders?status=PENDING every 30–60 seconds, and once after each reconnect. That is a few requests a minute instead of a few a second. On free, whose one WebSocket slot is the tier limit, give that slot to the execution feed if fills are what you are waiting for.
Best Practices
Use Batch Endpoints
POST /v1/orders/batch and POST /v1/prices/batch combine multiple
operations into one request — and a batch call counts as one tick
against your RPS/RPM. It’s bounded by your tier’s Max Batch Size
(see the Tiers table above): free=1 means no batching benefit on
free, so this pays off most on pro (5) / pro_plus (10) /
enterprise (25).Use WebSocket Feeds
Subscribe to
WS /v1/ws/prices instead of polling GET /v1/markets.
WebSocket connections don’t count against your REST rate limit.Cache Market Metadata
Market metadata (slug, question, outcomes) changes infrequently.
Cache it locally and only refresh periodically.
Idempotency Keys
On order placement (
POST /v1/orders, POST /v1/order,
POST /v1/clob/order), send an Idempotency-Key header — a
Stripe-style alias for the body’s client_order_id — so a retried
request can’t double-fill. Reusing a key with a different payload
returns 409 IDEMPOTENCY_KEY_REUSE; reuse it only for the exact same
order you’re retrying.Next Steps
- String Numerics — Why all numbers are strings
- Batch Orders — Reduce request count with batching