TL;DR: The Rodz API allows 100 requests per minute per API key. To stay within that budget, read the
X-RateLimit-Remainingheader before every call, implement exponential backoff on429responses, batch work through bulk endpoints, cache repeated lookups and queue outgoing requests with a token-bucket limiter. This guide walks through each strategy with production-ready Node.js and Python examples.
What are API rate limits?
Rate limits cap the number of requests a client can make to an API within a given time window. They exist for two reasons: protecting the platform from traffic spikes and guaranteeing a consistent experience for every user.
The Rodz API enforces a limit of 100 requests per minute per API key. Every response includes three headers that tell you exactly where you stand:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per window (100) |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
When you exceed the limit, the API responds with HTTP 429 Too Many Requests and a JSON body containing a retry_after field (in seconds). For the full error reference, see the Rodz API Reference.
Prerequisites
Before applying the strategies below, make sure you have:
- A valid Rodz API key. Generate one from your dashboard under Settings > API Keys. If you have not done this yet, follow the Getting Started guide.
- Node.js 18+ or Python 3.10+ installed locally (depending on which examples you plan to use).
- An HTTP client library. The examples use
axiosfor Node.js andhttpxfor Python. - Basic familiarity with async programming. Both sets of examples use async/await.
Strategy 1: Read the rate-limit headers
The simplest improvement you can make is to check X-RateLimit-Remaining after every response. If the value drops below a threshold, pause before sending the next request.
Node.js
import axios from 'axios';
const API_BASE = 'https://api.rodz.io/v1';
const API_KEY = process.env.RODZ_API_KEY;
const client = axios.create({
baseURL: API_BASE,
headers: { Authorization: `Bearer ${API_KEY}` },
});
async function safeFetch(url) {
const res = await client.get(url);
const remaining = parseInt(res.headers['x-ratelimit-remaining'], 10);
const resetAt = parseInt(res.headers['x-ratelimit-reset'], 10);
if (remaining <= 5) {
const waitMs = (resetAt - Math.floor(Date.now() / 1000)) * 1000;
console.log(`Rate limit low (${remaining} left). Pausing ${waitMs}ms.`);
await new Promise((resolve) => setTimeout(resolve, Math.max(waitMs, 0)));
}
return res.data;
}
Python
import os, time, httpx
API_BASE = "https://api.rodz.io/v1"
API_KEY = os.environ["RODZ_API_KEY"]
client = httpx.Client(
base_url=API_BASE,
headers={"Authorization": f"Bearer {API_KEY}"},
)
def safe_fetch(path: str) -> dict:
res = client.get(path)
res.raise_for_status()
remaining = int(res.headers["x-ratelimit-remaining"])
reset_at = int(res.headers["x-ratelimit-reset"])
if remaining <= 5:
wait = max(reset_at - int(time.time()), 0)
print(f"Rate limit low ({remaining} left). Pausing {wait}s.")
time.sleep(wait)
return res.json()
This approach is reactive. You only slow down when the budget is actually running low. For most integrations with moderate traffic, this alone is enough.
Strategy 2: Exponential backoff on 429 responses
Even with header monitoring, bursts can push you over the limit. When you receive a 429, the response body includes a retry_after value. The right pattern is to wait at least that long, then apply exponential backoff if the error repeats.
Node.js
async function fetchWithBackoff(url, maxRetries = 5) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const res = await client.get(url);
return res.data;
} catch (err) {
if (err.response?.status === 429) {
const retryAfter = err.response.data?.error?.retry_after ?? 5;
const backoff = retryAfter * Math.pow(2, attempt) * 1000;
console.log(`429 received. Retrying in ${backoff}ms (attempt ${attempt + 1}).`);
await new Promise((resolve) => setTimeout(resolve, backoff));
attempt++;
} else {
throw err;
}
}
}
throw new Error(`Failed after ${maxRetries} retries.`);
}
Python
import httpx, time, os
async def fetch_with_backoff(client: httpx.AsyncClient, path: str, max_retries: int = 5) -> dict:
for attempt in range(max_retries):
res = await client.get(path)
if res.status_code == 429:
retry_after = res.json().get("error", {}).get("retry_after", 5)
backoff = retry_after * (2 ** attempt)
print(f"429 received. Retrying in {backoff}s (attempt {attempt + 1}).")
await asyncio.sleep(backoff)
continue
res.raise_for_status()
return res.json()
raise RuntimeError(f"Failed after {max_retries} retries.")
The multiplier doubles the wait time with every consecutive failure. In practice you will rarely hit a third retry because the first pause is almost always sufficient.
Strategy 3: Use bulk endpoints
Every call to a bulk endpoint counts as a single request against your rate limit, regardless of how many items it contains (up to 100 per batch). If you are enriching companies or contacts, batching is the highest-leverage optimization available.
Instead of this:
// Bad: 50 requests consumed
for (const domain of domains) {
await client.get(`/enrichment/company?domain=${domain}`);
}
Do this:
// Good: 1 request consumed
const res = await client.post('/enrichment/company/bulk', {
items: domains.map((domain) => ({ domain })),
});
Bulk requests are processed asynchronously. You receive a job_id in the response and can either poll for results or set up a webhook to be notified when the job completes.
Strategy 4: Cache responses locally
Enrichment data for a given company or contact rarely changes within the same day. Caching responses eliminates redundant calls entirely.
Node.js (in-memory cache with TTL)
const cache = new Map();
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
async function cachedFetch(path) {
const cached = cache.get(path);
if (cached && Date.now() - cached.ts < CACHE_TTL) {
return cached.data;
}
const data = await fetchWithBackoff(path);
cache.set(path, { data, ts: Date.now() });
return data;
}
Python (with cachetools)
from cachetools import TTLCache
cache = TTLCache(maxsize=1024, ttl=3600)
async def cached_fetch(client: httpx.AsyncClient, path: str) -> dict:
if path in cache:
return cache[path]
data = await fetch_with_backoff(client, path)
cache[path] = data
return data
For production systems, replace the in-memory store with Redis or Memcached so the cache survives restarts and is shared across workers.
Strategy 5: Queue requests with a token bucket
A token bucket gives you fine-grained control over throughput. You start with a fixed number of tokens (matching the rate limit), consume one per request, and refill tokens at a steady rate. This smooths out bursts and keeps your usage predictable.
Node.js
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity;
this.tokens = capacity;
this.refillRate = refillRate; // tokens per second
this.lastRefill = Date.now();
}
async acquire() {
this.refill();
if (this.tokens < 1) {
const waitMs = ((1 - this.tokens) / this.refillRate) * 1000;
await new Promise((resolve) => setTimeout(resolve, waitMs));
this.refill();
}
this.tokens -= 1;
}
refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
this.lastRefill = now;
}
}
// 100 requests per 60 seconds = ~1.67 tokens/sec
const bucket = new TokenBucket(100, 100 / 60);
async function throttledFetch(url) {
await bucket.acquire();
return client.get(url).then((r) => r.data);
}
Python
import asyncio, time
class TokenBucket:
def __init__(self, capacity: int, refill_rate: float):
self.capacity = capacity
self.tokens = float(capacity)
self.refill_rate = refill_rate
self.last_refill = time.monotonic()
self._lock = asyncio.Lock()
async def acquire(self):
async with self._lock:
self._refill()
if self.tokens < 1:
wait = (1 - self.tokens) / self.refill_rate
await asyncio.sleep(wait)
self._refill()
self.tokens -= 1
def _refill(self):
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
self.last_refill = now
# 100 requests per 60 seconds
bucket = TokenBucket(capacity=100, refill_rate=100 / 60)
async def throttled_fetch(client: httpx.AsyncClient, path: str) -> dict:
await bucket.acquire()
res = await client.get(path)
res.raise_for_status()
return res.json()
Combining the strategies
In production, you want all five strategies working together. A typical request flow looks like this:
- Check the local cache. If a fresh result exists, return it immediately (zero API calls).
- Acquire a token from the bucket. This ensures you never burst above the limit.
- Make the API call and read the rate-limit headers.
- If the response is
429, apply exponential backoff and retry. - On success, store the result in the cache for future lookups.
For high-volume workloads, wrap this pipeline around bulk endpoints and use webhooks to receive results asynchronously. That combination can reduce your effective request count by 90% or more.
Frequently asked questions
What is the Rodz API rate limit?
The Rodz API allows 100 requests per minute per API key. This is a sliding window. The exact remaining budget and reset time are included in every response via the X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers.
What happens when I exceed the rate limit?
The API returns HTTP 429 Too Many Requests with a JSON body that includes a retry_after field (in seconds). Your client should wait at least that long before retrying. See the API Reference for the full error format.
Do bulk requests count as one request or many?
A single bulk request counts as one request against your rate limit, even if it contains up to 100 items. This makes bulk endpoints the most efficient way to enrich companies or contacts at scale.
Can I get a higher rate limit?
Yes. If your use case genuinely requires more than 100 requests per minute, contact the Rodz team to discuss enterprise plans with higher limits. Visit the API documentation for contact details.
Should I use polling or webhooks for bulk results?
Webhooks are the better choice for most production integrations. They eliminate the need to poll (which itself consumes rate-limit budget) and deliver results as soon as processing completes. Polling is fine during development or for one-off scripts.
How do I test rate-limit handling locally?
Send a burst of requests in a tight loop and observe how your backoff logic reacts to the 429 response. You can also mock the 429 response and the rate-limit headers in your test suite to verify the behavior without hitting the live API.
Does caching violate any API terms?
No. Caching responses on your side is encouraged. It reduces unnecessary calls, improves your application’s response time and helps the platform serve all users reliably. Just make sure you respect the freshness of the data for your specific use case.
Where can I find the full API documentation?
The complete endpoint reference, including authentication, error codes and pagination, is available in the Rodz API Reference. For interactive exploration, visit the API docs.