RankParse

Rate limiting

Request limits per API key, the separate daily caps on PageSpeed and Google Ads, the 429 response and how to back off.

RankParse applies two kinds of limits: a per-minute limit on every API key, and a few daily caps on endpoints that depend on a third party (Google PageSpeed Insights and Google Ads). Both return 429, but with different error codes and different advice.

Per-key request limit

Limit60 requests per minute per API key
WindowSliding 60 seconds, not a fixed calendar minute
ScopeEach API key has its own counter. Two keys in one workspace do not share it.
Rejected requestsDo not count toward the window and are never charged

The window slides: a request stops counting 60 seconds after it was made. If you send 60 requests in the first second, you can send again as soon as the oldest one is a minute old, not at the top of the next minute.

What counts

The limit applies to the credit-billed endpoints and to the rank tracking, Search Console, Google Ads keyword, outreach and approval inbox routes. A POST /v1/batch call is one request no matter how many domains it contains.

These account routes are not rate limited: /v1/credits, /v1/me, /v1/keys, /v1/usage and /v1/pricing, plus the connection status routes /v1/gsc/status and /v1/google-ads/status.

The 429 response

429 Too Many Requests
{
  "error": "Too Many Requests",
  "code": "rate_limited",
  "message": "60 requests/minute exceeded"
}
  • The body is the only signal. There are no X-RateLimit-* headers and no Retry-After header on a rate_limited response.
  • No credits are charged. The limit is checked before billing.
  • Authentication is checked first, so a bad key gets 401, never 429. The limit is checked before billing.

Higher limits are not sold per key. If your workload needs more than 60 requests per minute, group work with /v1/batch (up to 50 domains per request) or /v1/site-explorer, which returns backlinks, authority, top pages and anchor text in one call.

Daily caps

Some endpoints also have a daily allowance that resets at 00:00 UTC. These are separate from the per-minute limit and use their own error codes, so you can tell them apart.

EndpointCapScopeError codeCharged on failure?
GET /v1/page-performance50 calls per dayPer person (not per workspace)user_quota_exceededNo
GET /v1/page-performance25,000 fresh Google lookups per dayShared by all users, including the free page-speed toolquota_exceededNo
POST /v1/google-ads/keyword-ideas, POST /v1/google-ads/keyword-metrics, GET /v1/google-ads/accounts100 successful calls per dayPer persongoogle_ads_user_quota_exceededFree endpoint
Same Google Ads routes10,000 successful calls per dayShared by all usersgoogle_ads_daily_quota_exceededFree endpoint
429 user_quota_exceeded
{
  "error": "Too Many Requests",
  "code": "user_quota_exceeded",
  "message": "Maximum 50 page-performance calls per day reached"
}
429 google_ads_user_quota_exceeded
{
  "error": "Too Many Requests",
  "code": "google_ads_user_quota_exceeded",
  "message": "Daily Google Ads keyword limit reached (100/day). Resets at midnight UTC.",
  "retry_after": 41230
}

Details worth knowing:

  • PageSpeed per-user count. Every call counts toward the 50, including calls answered from the 24 hour result cache and calls that later fail.
  • Global PageSpeed quota is only consumed by calls that need a fresh Google lookup. When it is exhausted, quota_exceeded includes retry_after (seconds until 00:00 UTC).
  • Google Ads counts successes only. A call that fails upstream (for example google_ads_developer_token_not_approved) does not use your allowance. retry_after is the seconds until 00:00 UTC.
  • Google's own limit. If Google itself throttles the request you get google_ads_quota_exceeded, which is a different, transient condition. Retry later.
  • Do not retry daily caps in a loop. They cannot succeed until the reset.

Free tools

The public /v1/tools/* routes behind the RankParse website tools use their own rolling 24 hour allowance per tool: 5 uses per day for anonymous visitors (per IP) and 25 per day for signed-in free accounts. Exceeding it returns 429 quota_exceeded with a reset_at timestamp in milliseconds. These routes are meant for the website and require a captcha token. See Free tools.

Backing off

Treat a 429 as "slow down", not as a failure. A good client does three things:

  1. Pace itself so it rarely hits the limit. One request per second on average is safely under 60 per minute.
  2. Retries rate_limited with exponential backoff and jitter, capped at 60 seconds, because the window can need up to a minute to free a slot.
  3. Stops on daily caps and waits for the reset instead of retrying.
import os
import random
import time

import requests

API = "https://api.rankparse.com/v1"
HEADERS = {"X-API-Key": os.environ["RANKPARSE_API_KEY"]}

DAILY_CAP_CODES = {
    "quota_exceeded",
    "user_quota_exceeded",
    "google_ads_user_quota_exceeded",
    "google_ads_daily_quota_exceeded",
}


def get_with_backoff(path, params=None, max_attempts=6):
    for attempt in range(max_attempts):
        resp = requests.get(f"{API}{path}", params=params, headers=HEADERS, timeout=60)

        if resp.status_code != 429:
            resp.raise_for_status()
            return resp.json()

        code = resp.json().get("code")
        if code in DAILY_CAP_CODES:
            # A daily cap will not clear by retrying. Surface it.
            raise RuntimeError(f"Daily cap reached ({code}); resets at 00:00 UTC")

        # rate_limited: 1s, 2s, 4s, 8s, 16s, 32s, capped at 60s, plus jitter.
        delay = min(2 ** attempt, 60) + random.random()
        time.sleep(delay)

    raise RuntimeError("Still rate limited after retries")


# Stay under 60 requests per minute without relying on 429s.
domains = ["github.com", "stripe.com", "vercel.com"]
for domain in domains:
    result = get_with_backoff("/domain-authority", {"domain": domain})
    print(domain, result["data"]["score"])
    time.sleep(1)
const API = "https://api.rankparse.com/v1";
const HEADERS = { "X-API-Key": process.env.RANKPARSE_API_KEY };

const DAILY_CAP_CODES = new Set([
  "quota_exceeded",
  "user_quota_exceeded",
  "google_ads_user_quota_exceeded",
  "google_ads_daily_quota_exceeded",
]);

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function getWithBackoff(path, params = {}, maxAttempts = 6) {
  const url = `${API}${path}?${new URLSearchParams(params)}`;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const resp = await fetch(url, { headers: HEADERS });

    if (resp.status !== 429) {
      if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
      return resp.json();
    }

    const { code } = await resp.json().catch(() => ({}));
    if (DAILY_CAP_CODES.has(code)) {
      // A daily cap will not clear by retrying. Surface it.
      throw new Error(`Daily cap reached (${code}); resets at 00:00 UTC`);
    }

    // rate_limited: 1s, 2s, 4s, 8s, 16s, 32s, capped at 60s, plus jitter.
    const delay = Math.min(2 ** attempt, 60) + Math.random();
    await sleep(delay * 1000);
  }

  throw new Error("Still rate limited after retries");
}

// Stay under 60 requests per minute without relying on 429s.
for (const domain of ["github.com", "stripe.com", "vercel.com"]) {
  const result = await getWithBackoff("/domain-authority", { domain });
  console.log(domain, result.data.score);
  await sleep(1000);
}

Tips

  • Batch instead of looping. /v1/batch takes up to 50 domains in one request and counts as one request against the limit.
  • Use one key per service, so a runaway job cannot starve another one of its 60 requests per minute.
  • Cache on your side. Link graph data is a Common Crawl snapshot and does not change between calls. Repeating a call still costs credits. See Credits.
  • Parallelism needs a limiter. If you fan out requests across workers that share a key, throttle them together. They all draw from the same 60 per minute.
  • AI agents connected over MCP are subject to the same limit through their key or OAuth grant. See MCP server.

On this page