Skip to main content

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.
Because the windows are fixed rather than sliding, there is a boundary burst: a client aligned to the clock second can send its full per-second allowance just before the tick and again just after, briefly reaching about twice the nominal RPS. We do not treat that as abuse, but it is a property of the window rather than a documented allowance — do not design around it.The limiter also fails open: if Redis is unreachable the check returns full headroom instead of rejecting, so during a cache outage the stated limits are not enforced. That is a deliberate availability trade, stated here so the guarantee you are buying is the guarantee you actually get.

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.
The authoritative source for tier limits is GET /v1/keys/tiers. If a doc page ever disagrees with that endpoint, the endpoint wins.
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, a free 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.
When the day’s orders are spent, the order endpoints answer 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.
Distinguish this from RATE_LIMIT_EXCEEDED: that clears within a second, this clears at midnight UTC. Sleeping Retry-After on both is correct, but a bot that hits FREE_TIER_DAILY_LIMIT should stop placing orders for the day rather than spin — or move to a paid tier, which has no daily cap.
Like the rate limiter, the daily cap fails open: if Redis is unreachable the counter is neither enforced nor incremented, rather than blocking trading. The stated cap is the cap you get whenever the cache is healthy.

Rate Limit Response

When you exceed your limit, the API returns HTTP 429 with the standard two-field error envelope and a stable X-Polysim-Code response header:
Branch on 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 carries x-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 least Retry-After seconds, add jitter, and stop after a bounded number of attempts:
Do not wrap POST /v1/orders in a blind retry loop. A 5xx or a dropped connection does not prove the order was not placed. Retry an order only with the same client_order_id (or Idempotency-Key), so a retry cannot place a second order, or check GET /v1/orders first.
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 poll GET /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. On free (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=PENDING returns your open orders 50 to a page. Below 50 open orders that is one request. Above it, follow next_cursor while has_more is 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-second and 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:
  1. 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.
  2. Add Jitter: Apply randomized delays (e.g. 1.0s + random(0.1s, 0.5s)) to prevent synchronized polling bursts across multiple orders.
  3. Honor Retry-After: If a 429 occurs, parse the Retry-After header and back off for at least that long before retrying. Keep backing off while 429s continue (see Handling Rate Limits).
  4. Batch Fetching: Use GET /v1/data/orders or GET /v1/orders?status=PENDING to inspect all open orders in a single request rather than executing individual GET /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 a fill frame 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 (with auth.apiKey) streams live trade frames with status: "MATCHED".
A bot that listens on /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.
What the execution feed does not cover:
  • Fills only. It emits no cancellation or expiry events. Confirm those through REST.
  • At-least-once. A fill can arrive twice (for example live and again in a reconnect replay). De-duplicate on event_id.
  • Reconnects. Pass the last order_id you processed as ?watermark= when you reconnect, and the server replays up to 50 newer fills. See Execution Feed: reconnect without missing fills. Run the REST reconciliation after any gap longer than that.

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