Rate limits
10 requests per second, burst of 100 — a rate ceiling, not a monthly quota.
The preflop ranges API is limited to 10 requests per second sustained, with a burst allowance of 100 requests (a token bucket: the bucket holds 100 tokens and refills at 10/second).
- Keyed traffic is limited per subscription — all of your keys share one budget, so minting extra keys does not add throughput.
- Keyless traffic (the free discovery endpoints) is limited per client IP at the same rate.
There is no monthly quota. Mirroring an entire game tree is an expected use and finishes comfortably inside a night's run at the sustained rate.
Response headers
Every response carries the limit state:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Sustained rate (requests/second) — currently 10 |
X-RateLimit-Burst | Bucket size — currently 100 |
X-RateLimit-Remaining | Tokens left in your bucket right now |
Retry-After | On 429 only — seconds to wait before retrying |
These are exposed via Access-Control-Expose-Headers, so browser clients can read them cross-origin.
Going over
Over the limit returns 429 with a Retry-After header:
{
"error": "Rate limit exceeded — 10 requests/second (burst 100). Retry shortly.",
"docs": "https://docs.pokerdata.io"
}
Backing off for Retry-After seconds and retrying is always safe — every endpoint is read-only and idempotent.
for spot in $(cat spots.txt); do
until curl -sf "https://pokerdata.io/api/v1/ranges/nl/v2/range?stack=100&spot=$spot" \
-H "Authorization: Bearer $PDK" -o "out/$spot.json"; do
sleep 1
done
done
Caching
Responses are served with Cache-Control: private, max-age=300. The underlying solves are effectively immutable, so cache aggressively on your side — a spot you fetched once will not change out from under you.