Skip to main content

Rate Limits

Rate limits are enforced per API key using Redis sliding-window counters with both per-second (RPS) and per-minute (RPM) buckets.

Tiers

TierRequests/secRequests/minMax WS ConnectionsMax Batch Size
free212011
pro1060035
pro_plus301,8001010
enterprise1006,0005025
The free tier allows short bursts (up to 2 requests in any one second) but is also capped at 120 requests in any rolling one-minute window, 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.

Rate Limit Response

When you exceed your limit, the API returns HTTP 429 with the Polymarket-shape single-field error envelope and a stable X-Polysim-Code response header:
HTTP/1.1 429 Too Many Requests
Retry-After: 1
X-Polysim-Code: RATE_LIMIT_EXCEEDED
X-Request-Id: a1b2c3d4-...
Content-Type: application/json
{"error": "Rate limit exceeded. Retry after 1s."}
Branch on X-Polysim-Code === "RATE_LIMIT_EXCEEDED" rather than the body prose, and read the Retry-After header for the exact wait time in seconds.

Rate Limit Headers (on every response)

Every authenticated response carries x-ratelimit-* headers so bots can pre-throttle instead of waiting for an actual 429:
HeaderTypeDescription
x-ratelimit-tierstringTier of the key making the call — free / pro / pro_plus / enterprise
x-ratelimit-limitintPer-minute cap for the tier
x-ratelimit-limit-per-secondintPer-second cap for the tier
x-ratelimit-remainingintRequests remaining in the current rolling-minute window
x-ratelimit-remaining-per-secondintRequests remaining in the current rolling-second window
x-ratelimit-resetintUnix epoch seconds when the per-minute window rolls over
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.
import time
import requests

# Pre-throttle off the response headers — don't wait for 429.
resp = requests.get(url, headers={"X-API-Key": KEY})
remaining = int(resp.headers.get("x-ratelimit-remaining", "1"))
remaining_sec = int(resp.headers.get("x-ratelimit-remaining-per-second", "1"))
reset_at = int(resp.headers.get("x-ratelimit-reset", "0"))

# If the per-second bucket is almost drained, sleep a beat
if remaining_sec <= 1:
    time.sleep(1.0)
# If the rolling-minute bucket is almost drained, sleep until reset
elif remaining <= 5:
    sleep_for = max(0.0, reset_at - time.time())
    time.sleep(min(sleep_for, 60.0))

Handling Rate Limits

When the limiter does fire, exponential backoff keyed off Retry-After:
import time
import requests

def api_request(url, headers, json_data=None, max_retries=3):
    for attempt in range(max_retries):
        resp = requests.post(url, headers=headers, json=json_data)

        # 429 → respect Retry-After (always populated in seconds)
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 1))
            time.sleep(retry_after)
            continue

        if resp.status_code >= 500:
            time.sleep(2 ** attempt)  # Exponential backoff
            continue

        return resp

    raise Exception(f"Failed after {max_retries} retries")

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