Skip to main content
Conduit enforces rate limits per client, per required scope, in a fixed time window. Every /v1 response tells you where you stand through four response headers, and exceeding a limit returns a 429 rate_limited error with the relevant details. This page covers the model, the headers, the 429 body, the per-scope defaults, and how to back off and retry.

The model

Limits are counted along three dimensions at once:
client
dimension
Your API key. Each key has its own counter.
scope
dimension
The scope the route requires (data:read, ops:read, or admin). Each scope has its own limit, so heavy product traffic on data:read does not consume your ops:read budget.
window
dimension
A fixed time window. The current window length is 60 seconds. Your counter resets at the start of each window.
In other words: each key gets a separate quota for each scope, refreshed every window.

Rate limit headers

Every /v1 response (success or error) carries four headers describing your current budget. Read them on every response and let them drive your pacing.
x-ratelimit-policy
string
A description of the policy applied to this request (the limit and window in effect for the matched scope).
x-ratelimit-limit
integer
The maximum number of requests allowed in the current window for this client and scope.
x-ratelimit-remaining
integer
The number of requests you have left in the current window. When this reaches 0, further requests return 429 until the window resets.
x-ratelimit-reset
string (ISO 8601)
The timestamp when the current window resets and remaining refills. Use this to schedule retries.
Example response headers
x-ratelimit-policy: 1000 per 60s (scope data:read)
x-ratelimit-limit: 1000
x-ratelimit-remaining: 987
x-ratelimit-reset: 2026-06-24T18:42:00Z

The 429 response

When you exceed your limit, the request fails with HTTP 429 and the rate_limited code. The details object tells you which scope was limited, the ceiling, and the window length.
429 rate_limited
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded for scope 'data:read'.",
    "request_id": "8f1c2e90-7a4b-4c3d-9e21-2b6f0a1c4d55",
    "requestId": "8f1c2e90-7a4b-4c3d-9e21-2b6f0a1c4d55",
    "details": {
      "required_scope": "data:read",
      "limit": 1000,
      "window_seconds": 60
    }
  },
  "requestId": "8f1c2e90-7a4b-4c3d-9e21-2b6f0a1c4d55"
}
details.required_scope
string
The scope whose limit you exceeded.
details.limit
integer
The request ceiling for that scope in one window.
details.window_seconds
integer
The window length in seconds.

Defaults per scope

Default limits, applied per client over a 60 second window:
ScopeLimitWindowApplies to
data:read1000 requests60sProduct and data routes (most of the API).
ops:read500 requests60sOperational routes (liveness, source health, manifest).
admin250 requests60sSensitive admin routes. The admin scope also satisfies lower scopes.
These are defaults. Always trust the live x-ratelimit-* headers and the details in a 429 over any number hard-coded in your client, since the values in effect for your key are authoritative.

Backoff and retry

Retry on 429 and on 5xx (internal_error). Do not retry on 4xx other than 429: a bad_request, unauthorized, forbidden, or not_found will fail identically on retry, so fix the request instead. For a 429, the cleanest strategy is to wait until the window resets:
1

Detect

The response is 429, or x-ratelimit-remaining has hit 0.
2

Read the reset time

Take x-ratelimit-reset from the response headers (or compute from details.window_seconds).
3

Wait until reset

Sleep until that timestamp before retrying. This avoids hammering a closed window.
4

Retry, then escalate

For 5xx, use exponential backoff with jitter. Cap the number of attempts so a persistent failure surfaces instead of looping forever.
import os
import time
from datetime import datetime, timezone
import requests

URL = "https://data.quantoraresearch.com/v1/public/observations"
HEADERS = {"x-api-key": os.environ["CONDUIT_API_KEY"]}
PARAMS = {"country": "USA", "indicator": "cpi_inflation_yoy"}

def get_with_retry(max_attempts: int = 5):
    backoff = 1.0
    for attempt in range(max_attempts):
        resp = requests.get(URL, headers=HEADERS, params=PARAMS)

        if resp.status_code == 429:
            reset = resp.headers.get("x-ratelimit-reset")
            if reset:
                reset_at = datetime.fromisoformat(reset.replace("Z", "+00:00"))
                wait = max(0.0, (reset_at - datetime.now(timezone.utc)).total_seconds())
            else:
                wait = resp.json()["error"]["details"].get("window_seconds", 60)
            time.sleep(wait)
            continue

        if resp.status_code >= 500:
            time.sleep(backoff)
            backoff *= 2  # exponential backoff for 5xx
            continue

        resp.raise_for_status()
        return resp.json()

    raise RuntimeError("exhausted retries")
Stay ahead of the limit instead of reacting to it. Watch x-ratelimit-remaining and slow down before it hits 0. A short delay that keeps you under the ceiling beats a stall waiting out a 429.

Errors

The full error envelope and code table.

Response envelope

The shared shape of every response.