Rate Limits

Every FourA API request passes three checks before it reaches an engine: the limits of your own plan, then the platform's shared allowance for the endpoint you called, then the platform's shared allowance for all traffic. Each check can refuse a request on its own, and each one answers with a different body.

The Three Checks, in Order

  1. Plan limits. What your own plan allows: which endpoints and parameters it includes, how many requests may run at once per endpoint, how many per minute, how many browser requests per day, and the credits and bandwidth available in the billing period.
  2. Global platform limit. Everything the API host you called is handling at that moment, whichever endpoint the traffic went to. A refusal here reports "service": "api".
  3. Per-endpoint platform limit. Traffic on the single, proxy, or browser service you called.

Your own plan is judged first, and that ordering is the contract rather than an implementation detail. The shared allowances are common property, so a request the platform was always going to refuse must not spend them on the way to being refused. One account sending far more than its plan allows is cut before it touches anything anyone else draws on.

Checks 2 and 3 count FourA's total traffic, not yours. Read a refusal from either one as "FourA is busy", not as "you sent too much". Check 1 is about your account alone, and nothing else on the platform moves it.

A refusal from either shared check gives your account back everything its admission counted, the per-minute bucket and the browser day slot alike, because the request never reached a backend. It doesn't count toward the retry pause described under Requests per minute either: FourA's capacity refused it, not your plan.

POST /api/auto/ holds no slot of its own. The Single, Proxy and Browser sub-calls it makes for you pass all three checks like any other request, so a parallel batch of auto calls counts against your plan through its sub-calls. (Your request counts and success rate count the auto call itself, once; the sub-calls are shown as its attempts.)

Plan Limits

A plan limit answers with an X-FourA-Limit header naming which limit refused the call. The same code is in the body under reason, so you can branch on it without reading headers. Every plan-limit body carries error, reason, and documentation; the rest of the fields depend on the limit.

X-FourA-Limit Status What ran out
plan_limit_feature 403 The endpoint you called, or the exitCountries parameter, isn't in your plan
plan_limit_premium 403 exitClass: premium isn't in your plan
plan_limit_concurrency 429 Simultaneous requests on that endpoint
plan_limit_rate 429 Requests per minute on that endpoint
plan_limit_browser_daily 429 Browser requests for the day
plan_limit_credits 429 Billed credits for the billing period
plan_limit_bandwidth 429 Bandwidth for the billing period

The numbers behind each limit are your plan's, and the Limits & Features tab of Usage & Limits lists them next to your live usage. Don't hardcode them: every refusal carries the ceiling that refused it.

A refused request spends nothing. The outcome is rate_limit, and only success is billed.

Endpoint or parameter not in the plan

A 403 with plan_limit_feature means the call asked for something the plan doesn't include. The check runs before anything is counted, so the refused call doesn't touch your rate or daily counters.

{
  "error": "The browser endpoint is not included in your plan (Free). Upgrade to use it.",
  "reason": "plan_limit_feature",
  "documentation": "https://foura.ai/prices"
}

The same code and status answer a POST /api/proxy/ call that sets exitCountries on a plan without geo targeting. The error string names the parameter:

{
  "error": "Geo targeting (the exitCountries parameter) is not included in your plan (Free). Remove the parameter or upgrade.",
  "reason": "plan_limit_feature",
  "documentation": "https://foura.ai/prices"
}

plan_limit_premium is the same shape for exitClass: premium on a plan without premium exits. FourA may instead serve such a request from the standard pool and report exitClass: standard in the response, so handle both answers. Neither one spends a premium exit. See exitClass.

Neither 403 sets Retry-After. Waiting doesn't change the answer.

Simultaneous requests

Concurrency is counted per endpoint: your plan carries one ceiling for Single, one for Proxy, and one for Browser. The request that goes over comes back as 429 with Retry-After: 1:

{
  "error": "Concurrency limit reached: your plan allows 50 simultaneous proxy request(s). Retry when an in-flight request finishes.",
  "reason": "plan_limit_concurrency",
  "documentation": "https://foura.ai/prices",
  "limit": 50,
  "in_flight": 51,
  "retry_after_seconds": 1
}

in_flight counts the refused request too, so it reads at least one more than limit.

The fix is to bound your own parallelism rather than to retry harder. Answering a 429 by re-sending the same batch immediately produces another 429 for every call in it. See Run Requests in Parallel for a worked pattern.

Requests per minute

Single and Proxy carry a per-minute allowance, measured over a sliding minute. Only admitted requests count toward it: a refused request is taken back out, so an account that asks steadily a little over its allowance is served its allowance rather than refused almost everything.

{
  "error": "Rate limit reached: your plan allows 600 single requests per minute. Cool down and retry.",
  "reason": "plan_limit_rate",
  "documentation": "https://foura.ai/prices",
  "limit_per_minute": 600,
  "current_rate": 613,
  "retry_after_seconds": 17
}

retry_after_seconds is how long until one more request would be admitted, if you send nothing else in between: at least 1 second and at most 120. The Retry-After header carries the same value.

Retrying refused requests faster than that has a rule of its own. When the requests this allowance refused in the sliding minute go over twice the allowance, the call is refused with a 30-second pause instead:

{
  "error": "Rate limit cooldown: 1250 requests over your plan's 600 single requests per minute were refused in the last minute and kept arriving. Pause for 30 seconds.",
  "reason": "plan_limit_rate",
  "documentation": "https://foura.ai/prices",
  "limit_per_minute": 600,
  "current_rate": 540,
  "refused_last_minute": 1250,
  "cooldown": true,
  "retry_after_seconds": 30
}

Refusals during the pause aren't counted, so the pause ends on its own as the minute moves on, even for a client that keeps retrying. To tell the pause from the ordinary allowance, read cooldown rather than the error text.

Browser requests per day

Browser has no per-minute allowance. Its plan limit is a number of browser requests per day, counted from midnight UTC, and the meter counts every browser request admitted, not only successful ones.

{
  "error": "Daily browser limit reached: your plan allows 300 browser requests per day (resets at midnight UTC).",
  "reason": "plan_limit_browser_daily",
  "documentation": "https://foura.ai/prices",
  "limit_per_day": 300,
  "used_today": 301
}

This refusal carries no retry_after_seconds and no Retry-After header, because the wait is hours rather than seconds. Treat it as a stop and schedule the next run for midnight UTC.

Credits for the billing period

Only billed credits count, which means only successful requests. When the billed total reaches the credits available to you this period, further requests are refused until the period resets or you buy more.

{
  "error": "Credit limit reached: 75,000 of 75,000 credits used this billing period. Upgrade your plan or wait for the reset.",
  "reason": "plan_limit_credits",
  "documentation": "https://foura.ai/prices",
  "used": 75000,
  "hard_stop": 75000,
  "retry_after_seconds": 86400,
  "resets_at": "2026-10-01T00:00:00.000Z"
}

hard_stop is the billed-credit count at which requests stop for this period. Read it from the body rather than computing it: it already includes any credits you bought on top of the plan.

Bandwidth for the billing period

Plans that carry a bandwidth cap refuse requests once the standard traffic this period reaches it. Premium traffic has its own allowance and doesn't count toward this cap. Bought bandwidth counts the same as included bandwidth, and the error string quotes what's available to you, not what the plan alone includes.

{
  "error": "Bandwidth limit reached: 50 GB is available to you this billing period.",
  "reason": "plan_limit_bandwidth",
  "documentation": "https://foura.ai/prices",
  "used_bytes": 53687091200,
  "limit_bytes": 53687091200,
  "retry_after_seconds": 86400,
  "resets_at": "2026-10-01T00:00:00.000Z"
}

On both period limits, retry_after_seconds is capped at 24 hours; resets_at is the exact instant the period rolls over.

Plan limit fields

Field Type Present on Description
error string all Human-readable message, including the number you're held to
reason string all plan_limit_ plus the limit name. Same value as the X-FourA-Limit header.
documentation string all Link to the plans page
retry_after_seconds number concurrency, rate, credits, bandwidth How long to wait. Same value as the Retry-After header.
limit number concurrency Simultaneous requests the plan allows on that endpoint
in_flight number concurrency Requests running on that endpoint for your account, including the refused one
limit_per_minute number rate Requests per minute the plan allows on that endpoint
current_rate number rate Requests counted in the sliding minute, including the refused one
refused_last_minute number rate pause Requests the per-minute allowance refused in the sliding minute. Only on the 30-second pause.
cooldown boolean rate pause true on the 30-second pause for retrying too fast. Absent on an ordinary per-minute refusal.
limit_per_day number browser daily Browser requests the plan allows per day
used_today number browser daily Browser requests counted today, including the refused one
used number credits Billed credits so far this period
hard_stop number credits Billed credits at which requests stop this period
used_bytes number bandwidth Standard traffic so far this period, in bytes. Premium traffic isn't included.
limit_bytes number bandwidth Bytes available this period
resets_at string credits, bandwidth ISO 8601 timestamp of the period end

Plan limits use retry_after_seconds. The platform limits below use retryAfter. A retry helper needs to read both, or read the Retry-After header, which only plan limits set.

Platform Limits

The platform checks track two things per service and one more across all of them:

  • Concurrency: how many requests FourA is running at the same time.
  • RPM: how many requests FourA has taken in the last 60 seconds.

Both counters are shared by everyone using that service. current and limits in the responses below describe the platform, not your account. If you want your own number, read in_flight from a plan-limit response, or open Usage & Limits in the dashboard.

429: RPM Exceeded

{
  "error": "Rate limit exceeded",
  "status": 429,
  "service": "single",
  "retryAfter": 5,
  "current": {
    "concurrency": 12,
    "rpm": 3000
  },
  "limits": {
    "maxConcurrency": 500,
    "maxRpm": 3000
  }
}

The service took its allowed requests for the last minute. Wait retryAfter seconds.

503: Concurrency Exceeded

{
  "error": "Service at capacity",
  "status": 503,
  "service": "proxy",
  "retryAfter": 2,
  "current": {
    "concurrency": 500,
    "rpm": 1200
  },
  "limits": {
    "maxConcurrency": 500,
    "maxRpm": 3000
  }
}

The service is running as many requests as it's allowed to run at once. This clears in seconds.

Service Disabled

When a service is temporarily taken offline for maintenance, the API returns 503 with a different error message:

{
  "error": "Service disabled",
  "status": 503,
  "service": "single",
  "retryAfter": 60,
  "current": { "concurrency": 0, "rpm": 0 },
  "limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}

This isn't a rate limit. The service is temporarily unavailable. Check the retryAfter value and retry after that many seconds. This typically resolves within minutes.

Both 503 shapes carry the same keys, so branch on the error string and never on which fields are present. Service disabled is maintenance, Service at capacity is concurrency.

On the maintenance shape, current.concurrency and current.rpm are always 0: the request was turned away before anything was measured.

Platform Limit Fields

Field Type Description
error string Human-readable error message
status number HTTP status code (429 or 503)
service string Which service refused the call: single, proxy, browser, or api
retryAfter number Recommended wait time in seconds before retrying
current.concurrency number Requests the service was running platform-wide when it refused
current.rpm number Requests the service took platform-wide in the last 60 seconds
limits.maxConcurrency number The service's platform-wide concurrency allowance
limits.maxRpm number The service's platform-wide per-minute allowance

Handling Every Refusal With One Helper

Retry-After is set on the plan limits that are worth waiting for, retry_after_seconds is in their bodies, and retryAfter is in the platform bodies. Read all three in that order, and stop on the plan limits that no wait will clear:

import time
import requests

# Plan limits that a short wait never clears.
STOP_ON = {
    "plan_limit_feature",
    "plan_limit_premium",
    "plan_limit_browser_daily",
    "plan_limit_credits",
    "plan_limit_bandwidth",
}

def wait_seconds(resp, attempt):
    header = resp.headers.get("Retry-After")
    if header and header.isdigit():
        return int(header)
    try:
        body = resp.json()
    except ValueError:
        return 2 ** attempt
    return body.get("retry_after_seconds") or body.get("retryAfter") or 2 ** attempt

def fetch(url, api_key, max_retries=5):
    for attempt in range(max_retries):
        resp = requests.post(
            "https://eu.api.foura.ai/api/single/",
            headers={"X-API-Key": api_key, "Content-Type": "application/json"},
            json={"method": "GET", "url": url},
        )

        limit = resp.headers.get("X-FourA-Limit")
        if limit in STOP_ON:
            raise RuntimeError(f"stopped by plan limit {limit}: {resp.json().get('error')}")

        if resp.status_code in (429, 503):
            time.sleep(wait_seconds(resp, attempt))
            continue

        return resp

    raise RuntimeError("Max retries exceeded")

A daily allowance doesn't come back for hours, and a period allowance doesn't come back for days, so treat them as a stop rather than a wait. Read resets_at from the body if you want to schedule the next run.

Tips

  • Cap the number of requests you keep in flight instead of retrying a refused batch. A retry storm turns one 429 into many.
  • Read X-FourA-Limit first. It tells you in one string whether the limit is yours or the platform's, and no platform refusal sets it.
  • Don't hardcode the numbers. Every plan-limit response carries the ceiling that refused it, and Usage & Limits shows all of them.
  • retryAfter on platform limits is fixed by kind: 2 seconds for concurrency, 5 for RPM, 60 for maintenance.
  • Match on error to tell the two 503s apart. Both shapes carry current and limits, so a check for "are those fields there?" reads maintenance as a concurrency problem.
  • A 403 with X-FourA-Limit is about your plan, not about the target site. The target never answered.

The Proxy Port Has Its Own Numbers

Everything above is about the JSON API. Traffic you send through proxy.foura.ai is held to a separate set of plan numbers, in a different unit: tunnels open at once, tunnel openings a minute, and the standard traffic for the billing period. Those refusals arrive as an HTTP status with an X-Foura-Error header rather than as a JSON body, because a CONNECT has no body to put one in. See Proxy Port for the status table and How Your Plan Is Metered for which pool the port's gigabytes come out of.

Last updated: September 30, 2026