Rate Limits
Per-plan request-per-second limits, stealth throttling, and retry guidance.
Stealth mode will no longer be available from 1 November 2026. Search and Extract are unchanged.
Serpex enforces per-organization rate limits based on your highest completed purchase. Limits are exact — there is no burst allowance.
Limits by tier
Prop
Type
The tier is set by your largest single completed purchase, not your current balance or the sum of purchases.
Stealth mode cap
Stealth extract requests ("stealth": true on /api/crawl) are additionally rate-capped to a fixed global cap (default: 10 concurrent stealth requests). Requests that exceed this cap are queued, not rejected — they will resolve once a slot is available.
Stealth requests may take a few seconds longer when the queue is busy. Factor this into your timeout settings. No extra credits are charged for queue wait time.
HTTP response codes
| Code | Meaning | Action |
|---|---|---|
200 | Success | — |
401 | Invalid or missing API key | Fix the key; don't retry |
402 | Insufficient credits | Top up at app.serpex.dev |
429 | Rate limit exceeded | Back off and retry (see below) |
500 / 503 | Transient server error | Retry with exponential backoff |
The 429 response includes Retry-After and X-RateLimit-Limit headers:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 50
X-RateLimit-Remaining: 0
Retry-After: 1Retry strategy
Implement exponential backoff for 429 and 5xx responses:
async function searchWithRetry(client: SerpexClient, query: string, maxRetries = 4) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await client.search({ q: query });
} catch (err: any) {
const status = err?.status ?? err?.response?.status;
if (status === 429 || status === 503) {
const waitMs = Math.min(1000 * 2 ** attempt, 16000);
await new Promise((r) => setTimeout(r, waitMs));
continue;
}
throw err; // non-retryable (401, 402, 400, …)
}
}
throw new Error("Max retries exceeded");
}import time
def search_with_retry(client, query: str, max_retries: int = 4):
for attempt in range(max_retries):
try:
return client.search({"q": query})
except Exception as e:
status = getattr(e, "status_code", None)
if status in (429, 503):
wait = min(1 * (2 ** attempt), 16)
time.sleep(wait)
continue
raise # non-retryable
raise RuntimeError("Max retries exceeded")Do not retry 401 (bad key) or 402 (no credits). Those errors won't resolve on their own.
High-volume usage
If your workload requires sustained throughput above the Scale tier, contact us to discuss an Enterprise plan with a custom limit.