Rate Limits
Per-route, per-user rate limits across the MisarSEO API, and which endpoints have none.
MisarSEO rate limits per user, per route. Each route has its own independent fixed window — spending your budget on keyword research does not affect your crawl budget.
Exceeding a limit returns 429 with code RATE_LIMITED and a Retry-After header in seconds.
Per user, not per IP
The limit key is the resolved caller. Sharing an API key across machines shares the budget; using separate keys for the same account does not increase it.
Limits by route
| Route | Method | Limit |
|---|---|---|
/seo/projects | POST | 30 / 60 s |
/seo/projects/:projectId | PATCH, DELETE | 30 / 60 s |
/seo/crawl/start | POST | 5 / 60 s |
/seo/crawl/:jobId/stream | GET | 10 / 60 s |
/seo/keywords/research | POST | 10 / 60 s |
/seo/keywords/saved | POST | 30 / 60 s |
/seo/rank-tracking/configs | POST, PATCH, DELETE | 30 / 60 s |
/seo/rank-tracking/keywords | POST, DELETE | 30 / 60 s |
/seo/rank-tracking/runs | GET | 60 / 60 s |
/seo/domain/overview | GET | 20 / 60 s |
/seo/backlinks | GET | 20 / 60 s |
/seo/competitors | POST | 10 / 60 s |
/seo/ai/brand-lookup | POST | 5 / 60 s |
/seo/ai/prompt-explorer | POST | 10 / 60 s |
/seo/gsc/performance | POST | 20 / 60 s |
/seo/gsc/inspect-urls | POST | 10 / 60 s |
/seo/local/business-listings | POST | 20 / 60 s |
/seo/local/questions-answers | POST | 20 / 60 s |
/seo/account/export | GET | 5 / 60 s |
/seo/account/delete | DELETE, POST | 3 / 3600 s |
Routes with no rate limit
These are cheap reads or utility endpoints and are not throttled. They are still subject to plan quotas.
GET /seo/projectsGET /seo/projects/:projectIdGET /seo/crawl/:jobId/statusGET /seo/keywords/savedGET /seo/rank-tracking/configsGET /seo/rank-tracking/keywordsGET /seo/ai/brand-lookup/:jobId/statusGET /seo/gsc/connectGET /api/healthGET,POST,DELETE/api/keysPOST /seo/citations
Poll status, do not poll the stream
GET /seo/crawl/:jobId/status and GET /seo/ai/brand-lookup/:jobId/status are unthrottled precisely so they can be polled. The SSE stream is capped at 10 opens per minute — open one and hold it, rather than reconnecting in a loop.
Plan quotas are separate
A 429 can also mean a plan quota, not a request-rate limit:
| Code | Meaning |
|---|---|
RATE_LIMITED | Too many requests in the window. Wait out Retry-After. |
PLAN_LIMIT_REACHED | A plan allowance is exhausted, e.g. tracked-keyword count. Waiting does not help — upgrade or free capacity. |
AUDIT_CAPACITY_REACHED | Too many audits queued or running. Wait for one to finish. |
Always read code before deciding whether to retry.
Handling
async function withRetry(run: () => Promise<Response>, attempts = 3) {
for (let i = 0; i < attempts; i++) {
const res = await run();
if (res.status !== 429) return res;
const body = await res.clone().json().catch(() => ({}));
// Waiting never clears a plan quota.
if (body.code === "PLAN_LIMIT_REACHED") return res;
const wait = Number(res.headers.get("Retry-After") ?? 5);
await new Promise((r) => setTimeout(r, wait * 1000));
}
return run();
}See Errors for the full code table.