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

PlanRPM (per minute)RPD (per day)WS connectionsSymbols per WS connection
Basic3010,0002200
Standard12050,00010200
Pro600250,00030500
EnterpriseCustomCustomCustomCustom

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:

LimitField on /v1/usageEnforced
Concurrent connectionslimits.ws_connectionsAt the HTTP upgrade — exceeding it returns 429 TOO_MANY_CONNECTIONS
Subscriptions per connectionlimits.ws_symbolsAt 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