Параллельное выполнение request

Что вы узнаете

Как выполнять большой пакет запросов FourA параллельно без накопления ошибок 429, ограничивая число одновременно открытых вызовов и реагируя на отказ вместо его повторения.

Предварительные требования

Лимиты, с которыми вы работаете

Ваш тарифный план устанавливает два ограничения для каждого endpoint: сколько запросов может выполняться одновременно и сколько может запускаться в минуту (для Browser действует лимит на день вместо лимита на минуту). Для Single, Proxy и Browser действуют собственные значения, и они перечислены на вкладке Limits & Features в разделе Usage & Limits.

Запрос, превышающий лимит параллельности, возвращается с кодом HTTP 429 с X-FourA-Limit: plan_limit_concurrency и значением лимита в теле ответа:

{
  "error": "Concurrency limit reached: your plan allows 50 simultaneous single 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
}

Запрос, превышающий лимит в минуту, возвращает X-FourA-Limit: plan_limit_rate, а retry_after_seconds сообщает, через сколько секунд будет пропущен еще один запрос (от 1 до 120 секунд).

Оба отказа происходят мгновенно. FourA не ставит вызов в очередь для последующего выполнения, поэтому ресурсы не расходуются и плата не взимается. Отклоненный вызов не учитывается в скользящем минутном окне, поэтому аккаунт, немного превышающий лимит, все равно использует свой лимит полностью. Для шторма повторных запросов предусмотрен отдельный ответ: когда отклоненные вызовы продолжают поступать со скоростью более чем в два раза выше поминутного лимита, каждый вызов к этому endpoint отклоняется с cooldown: true и retry_after_seconds: 30, поэтому сделайте паузу на эти 30 секунд. Полное описание полей: Rate Limits.

Два важных нюанса, которые часто упускают:

  • Вызов POST /api/auto/ сам по себе слот не занимает, но каждый подвызов Single, Proxy и Browser, выполняемый им для вас, занимает. Один вызов auto может занимать более одного слота во время выполнения каскада. (В вашей панели управления вызов auto считается как request, а его подвызовы считаются попытками.)
  • Запрос Browser занимает свой слот все время, пока рендерится страница, что намного дольше, чем запрос Single. Пакет вызовов browser исчерпает лимит меньшим числом запросов, чем пакет вызовов single.

Шаг 1: Ограничьте собственный уровень параллелизма

Выберите число ниже лимита для вызываемого endpoint и придерживайтесь его. В пуле воркеров это настраивается одной строкой:

import os
import concurrent.futures
import requests

API = "https://eu.api.foura.ai/api/single/"
KEY = os.environ["FOURA_API_KEY"]
HEADERS = {"X-API-Key": KEY, "Content-Type": "application/json"}

# Below your plan's Single concurrency, so a slow request never pushes the batch over it.
MAX_IN_FLIGHT = 30

def fetch_one(url):
    resp = requests.post(API, headers=HEADERS, json={"method": "GET", "url": url}, timeout=60)
    return url, resp.status_code, resp.json()

def fetch_all(urls):
    results = []
    with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_IN_FLIGHT) as pool:
        for outcome in concurrent.futures.as_completed(pool.submit(fetch_one, u) for u in urls):
            results.append(outcome.result())
    return results

max_workers это весь механизм. В пуле никогда не бывает больше указанного числа открытых вызовов, поэтому пакет может содержать миллион URL и при этом не превышать лимит.

Оставляйте запас. Если два процесса на вашей стороне используют один и тот же аккаунт, даже с разными API ключами, они делят общий лимит, поэтому выделите каждому по половине. Если быстрый целевой ресурс позволяет пулу завершать requests быстрее, чем допускает минутный лимит, регулируйте скорость workers, чтобы не превышать и ограничение в минуту.

Шаг 2: экспоненциальная задержка вместо повторной отправки

Если отказ все же получен, неправильное решение, сразу отправлять пакет повторно. Каждый вызов в нем снова получит отказ, и повторные попытки будут накладываться на запросы, которые уже выполнялись.

Сначала подождите. Время ожидания указано в header Retry-After и в поле retry_after_seconds в body:

import time

# Plan limits that no short wait will clear.
STOP_ON = {"plan_limit_browser_daily", "plan_limit_credits", "plan_limit_bandwidth", "plan_limit_feature", "plan_limit_premium"}

def fetch_one(url, attempts=4):
    for attempt in range(attempts):
        resp = requests.post(API, headers=HEADERS, json={"method": "GET", "url": url}, timeout=60)

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

        if resp.status_code not in (429, 503):
            return url, resp.status_code, resp.json()

        header = resp.headers.get("Retry-After")
        if header and header.isdigit():
            wait = int(header)
        else:
            body = resp.json()
            wait = body.get("retry_after_seconds") or body.get("retryAfter") or 2 ** attempt
        time.sleep(wait)

    return url, 429, {"error": "still refused after retries"}

Добавляйте джиттер при запуске большого количества воркеров. Без него каждый воркер, получивший отказ в одну и ту же секунду, проснется в ту же секунду, и они снова одновременно получат отказ.

import random
time.sleep(wait + random.uniform(0, 0.5))

Шаг 3. То же самое на Node

const API = 'https://eu.api.foura.ai/api/single/';
const HEADERS = {
  'X-API-Key': process.env.FOURA_API_KEY,
  'Content-Type': 'application/json',
};
const MAX_IN_FLIGHT = 30;

async function fetchOne(url) {
  const resp = await fetch(API, {
    method: 'POST',
    headers: HEADERS,
    body: JSON.stringify({ method: 'GET', url }),
  });
  return { url, status: resp.status, body: await resp.json() };
}

async function fetchAll(urls) {
  const queue = [...urls];
  const results = [];

  async function worker() {
    while (queue.length) {
      results.push(await fetchOne(queue.pop()));
    }
  }

  await Promise.all(
    Array.from({ length: Math.min(MAX_IN_FLIGHT, urls.length) }, worker)
  );
  return results;
}

Фиксированное количество воркеров, берущих задачи из одной очереди, держит открытым ровно столько вызовов, независимо от размера пачки.

Шаг 4: Отслеживайте фактическое использование

Откройте раздел Usage & Limits в панели управления. Счетчики реального времени рядом с лимитами показывают параллелизм, минутный rate limit и запросы браузера в день по мере их изменения, поэтому вы можете настроить MAX_IN_FLIGHT под реальный запуск, а не наугад.

В Activity Log также фиксируются отклоненные запросы. Если запуск завершился с нужным количеством строк, но с разрозненными результатами rate_limit, значит, происходил троттлинг.

Распространенные ошибки

  • Отправка всего списка сразу. Запуск asyncio.gather по 5 000 URL или Promise.all по неограниченному массиву открывает 5 000 вызовов. Ограничивайте пул, а не список.
  • Параллельные повторные попытки. Немедленная повторная отправка каждого отклоненного вызова повторяет всплеск, вызвавший отказ. Подождите время из Retry-After и добавьте джиттер.
  • Отношение к дневному или периодическому лимиту как к временной паузе. plan_limit_browser_daily сбрасывается в полночь по UTC, а plan_limit_credits и plan_limit_bandwidth сбрасываются в конце расчетного периода. Остановите запуск и прочитайте resets_at из тела ответа, если оно присутствует.
  • Подсчет auto-вызовов как одного слота. Логика внутри /api/auto/ выполняет реальные подвызовы, и именно они учитываются в лимитах.
  • Использование одного ключа в разных процессах без разделения бюджета. Лимиты действуют на аккаунт, а не на отдельный процесс.
  • Настройка одного пула для всех endpoint. Для Single, Proxy и Browser действуют отдельные лимиты параллелизма. Пул, настроенный для Single, превысит меньший лимит Browser.
  • Rate Limits: форматы всех отклонений и значение каждого поля
  • Response Headers: X-FourA-Limit и Retry-After
  • Request Outcomes: почему результат rate_limit никогда не тарифицируется
  • Usage & Limits: где находятся счетчики реального времени и параметры вашего плана
  • Smart Fetch (Auto): как один auto-вызов превращается в несколько подвызовов
Обновлено: 15 октября 2026 г.