Rate Limits
Understand Nodukan API rate limits — request windows, plan tiers, response headers, 429 handling, and exponential backoff strategies.
Rate Limits
The Nodukan API enforces rate limits on a per-key, 60-second sliding window to ensure fair usage and platform stability. When you exceed the limit, you receive a 429 Too Many Requests response with headers telling you exactly when to retry.
Limits by plan#
| Plan | Requests / 60 s | Burst (10 s window) |
|---|---|---|
| Free | 60 | 15 |
| Pro | 300 | 60 |
| Scale | 1 000 | 200 |
| Custom | Negotiated | Negotiated |
Limits are per API key, not per account. If you generate multiple keys, each key has its own independent limit bucket.
Rate limit headers#
Every API response includes these headers so you can track your current usage:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets |
Retry-After | Seconds to wait before retrying (only on 429 responses) |
Example response headers on a normal (non-throttled) request:
HTTP/1.1 200 OK
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 247
X-RateLimit-Reset: 1715000460
Content-Type: application/json
429 response#
When you're throttled, the full response looks like this:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1715000460
Retry-After: 18
Content-Type: application/json
{
"error": {
"type": "rate_limit_error",
"code": "too_many_requests",
"message": "You have exceeded your rate limit of 300 requests per 60 seconds. Retry after 18 seconds.",
"retryAfter": 18,
"resetAt": "2026-05-06T10:01:00Z"
},
"requestId": "req_01j9x..."
}
Retry strategy#
The recommended approach is exponential backoff with jitter. Read the Retry-After header and wait at least that many seconds. If Retry-After is absent, fall back to an exponential schedule.
JavaScript / Node.js#
async function fetchWithRetry(url, options, maxRetries = 4) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429) return res;
if (attempt === maxRetries) {
throw new Error(`Rate limit exceeded after ${maxRetries} retries`);
}
const retryAfter = Number(res.headers.get('Retry-After') ?? 0);
const jitter = Math.random() * 1000; // 0–1000 ms
const delay = retryAfter * 1000 || Math.min(1000 * 2 ** attempt, 30_000);
console.warn(`Rate limited. Waiting ${Math.round((delay + jitter) / 1000)}s…`);
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
}
}
// Usage
const res = await fetchWithRetry('https://api.nodukan.com/v1/lead-magnets', {
headers: { Authorization: `Bearer ${process.env.NODUKAN_API_KEY}` },
});
Python#
import os, time, random, requests
def fetch_with_retry(url, headers, max_retries=4):
for attempt in range(max_retries + 1):
resp = requests.get(url, headers=headers)
if resp.status_code != 429:
resp.raise_for_status()
return resp.json()
if attempt == max_retries:
raise RuntimeError(f"Rate limit exceeded after {max_retries} retries")
retry_after = float(resp.headers.get("Retry-After", 0))
delay = retry_after or min(2 ** attempt, 30)
jitter = random.uniform(0, 1)
print(f"Rate limited. Waiting {delay + jitter:.1f}s…")
time.sleep(delay + jitter)
data = fetch_with_retry(
"https://api.nodukan.com/v1/lead-magnets",
headers={"Authorization": f"Bearer {os.environ['NODUKAN_API_KEY']}"},
)
PHP#
<?php
function fetchWithRetry(string $url, string $apiKey, int $maxRetries = 4): array {
for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 429) {
return json_decode($body, true);
}
if ($attempt === $maxRetries) {
throw new RuntimeException("Rate limit exceeded after $maxRetries retries");
}
// Parse Retry-After from headers (simplified — use CURLOPT_HEADERFUNCTION in prod)
$delay = min(pow(2, $attempt), 30) + (rand(0, 1000) / 1000.0);
echo "Rate limited. Waiting {$delay}s…\n";
usleep((int)($delay * 1_000_000));
}
}
Best practices#
- Pre-emptively throttle — if you're iterating over a large result set, add a small delay between requests rather than burning through your window.
- Batch where possible — use the
limitparameter (up to 100) to reduce the number of requests needed to paginate through data. - Cache responses — lead magnet and account data rarely changes. Cache aggressively and refresh on a schedule rather than re-fetching on every operation.
- Use webhooks — instead of polling for new leads or chat events, subscribe to webhooks to receive push notifications only when something changes.
If you consistently hit rate limits on the Pro plan, contact us about Scale or Custom plans with higher limits.
Last updated