Rate Limiting
The API enforces two independent rate-limit windows: per minute and per day. Both are checked on every REST request.
WebSocket connections are governed separately — see WebSocket limits below.
Default Limits by Plan
| Plan | RPM (per minute) | RPD (per day) | WS connections | Symbols per WS connection |
|---|---|---|---|---|
| Basic | 30 | 10,000 | 2 | 200 |
| Standard | 120 | 50,000 | 10 | 200 |
| Pro | 600 | 250,000 | 30 | 500 |
| Enterprise | Custom | Custom | Custom | Custom |
WebSocket Limits
WebSocket connections are not governed by RPM/RPD. Opening a connection does not
consume your request budget, and the bar/tick messages you receive are not counted as
requests. Two separate limits apply instead:
| Limit | Field on /v1/usage | Enforced |
|---|---|---|
| Concurrent connections | limits.ws_connections | At the HTTP upgrade — exceeding it returns 429 TOO_MANY_CONNECTIONS |
| Subscriptions per connection | limits.ws_symbols | At subscribe — symbols over the cap come back in the rejected array |
There is no separate limit on connection attempts — only on concurrent connections. A reconnect loop will not get you throttled further or blocked. It will also never succeed on its own, though: if you are at the cap, retrying cannot help until a slot frees, so back off rather than spinning.
Refused upgrades are counted in your rate_limited usage metric.
Rate-Limit Headers
Every response includes current consumption data so you can self-throttle:
X-RateLimit-Limit-Minute: 120
X-RateLimit-Remaining-Minute: 98
X-RateLimit-Reset-Minute: 1749600060
X-RateLimit-Limit-Day: 50000
X-RateLimit-Remaining-Day: 48766
429 Response
When you exceed a limit, the API returns:
HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Exceeded: minute
Content-Type: application/json
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded (minute). Retry after 42 seconds."
}
}
Always read Retry-After — it gives the exact number of seconds to wait, never guess.
Exponential Backoff
import time
import random
import requests
def api_get(url, headers, params=None, max_retries=5):
"""GET with exponential backoff on 429."""
for attempt in range(max_retries):
resp = requests.get(url, headers=headers, params=params)
if resp.status_code == 200:
return resp.json()
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 60))
jitter = random.uniform(0, 5)
wait = retry_after + jitter
print(f"Rate limited (attempt {attempt + 1}) — waiting {wait:.1f}s")
time.sleep(wait)
continue
# Don't retry on other 4xx errors
resp.raise_for_status()
raise RuntimeError("Max retries exceeded")
Proactive Self-Throttling
Instead of waiting for a 429, monitor the headers and slow down proactively:
import time
import requests
class RateLimitedClient:
def __init__(self, api_key, rpm=120):
self.session = requests.Session()
self.session.headers.update({"X-API-Key": api_key})
self.min_interval = 60.0 / rpm
self.last_call = 0.0
self.remaining = rpm
def get(self, url, params=None):
# If close to the limit, wait for the minute to reset
if self.remaining <= 5:
wait = 60 - (time.time() % 60) + 1
print(f"Approaching limit — pausing {wait:.0f}s")
time.sleep(wait)
# Enforce minimum interval between calls
elapsed = time.time() - self.last_call
if elapsed < self.min_interval:
time.sleep(self.min_interval - elapsed)
resp = requests.get(url, headers=self.session.headers, params=params)
self.remaining = int(resp.headers.get("X-RateLimit-Remaining-Minute", 999))
self.last_call = time.time()
return resp
client = RateLimitedClient("fxd_your_key", rpm=120)
Best Practices
1 — Cache closed candles
Closed bars never change. Cache them locally rather than re-fetching:
import functools
@functools.lru_cache(maxsize=2000)
def get_daily_candles(symbol, date_str):
"""Cache daily OHLCV by symbol + date."""
return api.history(symbol, "1D", from_date=date_str, to_date=date_str)
2 — Use WebSocket for live prices
Don't poll /v1/history every second to check for new bars. Subscribe to the WebSocket instead — you get pushed updates the instant each M1 bar closes.
3 — Fetch large batches, not many small ones
Instead of 10 separate requests for 10 symbols, batch using countback:
# Don't do this — 10 requests
for symbol in symbols:
data = client.history(symbol, "60", countback=10)
# Better — one larger request per symbol but fetch more at once
data = client.history("WMMARKETS:ZERO:EURUSD", "60", countback=500)
4 — Stagger parallel requests
If you must make multiple simultaneous requests, add a small delay between them to avoid burst spikes:
import time
for symbol in symbols:
process(client.history(symbol, "60", 100))
time.sleep(0.1) # ~10 requests/second = well under 120 RPM