Паралелно изпълнение на заявки

Какво ще научите

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

Предварителни изисквания

Лимитите, с които се съобразявате

Вашият план съдържа два тавана за всеки endpoint: колко requests могат да се изпълняват едновременно и колко могат да стартират за минута (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 не поставя повикването на опашка, за да го върне по-късно, така че нищо не се изразходва и нищо не се таксува. Отказаното повикване не се брои към плъзгащата се минута, така че акаунт, който изисква малко над тавана си, все още получава пълния таван. Бурята от повторни опити (retry storm) получава собствен отговор: когато отказаните повиквания продължават да пристигат с повече от два пъти над тавана ви за минута, всяко повикване към този endpoint се отказва с cooldown: true и retry_after_seconds: 30, така че направете пауза за тези 30 секунди. Пълна справка за полетата: Rate Limits.

Две неща от значение, които често се пропускат:

  • Едно POST /api/auto/ повикване не заема слот само по себе си, но всяко Single, Proxy и Browser подповикване, което прави за вас, заема. Едно auto повикване може да заема повече от един слот, докато стъпковата му логика (ladder) се изпълнява. (В таблото ви auto повикването е заявката; неговите подповиквания са неговите опити.)
  • Една Browser заявка заема своя слот за времето, необходимо на страницата да се рендерира, което е значително по-дълго от Single заявка. Партида от browser повиквания запълва тавана си с по-малко заявки, отколкото партида от single повиквания.

Стъпка 1: Ограничете собствената си паралелност

Изберете число под тавана за endpoint, който извиквате, и го поддържайте. Пул от работни процеси (worker pool) прави това на един ред:

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 ключове, те споделят общ таван, така че дайте на всеки от тях половината. Ако бърза крайна точка позволява на пула да завършва заявките по-бързо, отколкото една минута допуска, регулирайте темпото на работните процеси, за да останете и под минутния таван.

Стъпка 2: Изчакайте с отстъпление вместо незабавно повторно изпращане

Ако се получи отказ, грешният подход е незабавното повторно изпращане на партидата. Всяко извикване в нея ще бъде отказано отново, а повторните опити ще се натрупат върху заявките, които вече се изпълняват.

Първо изчакайте. Времето за изчакване е посочено в хедъра Retry-After и в retry_after_seconds в тялото на отговора:

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"}

Добавете jitter, когато изпълнявате много workers. Без него всеки worker, получил отказ в една и съща секунда, се събужда в същата секунда и отново получава отказ заедно с останалите.

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;
}

Фиксиран брой workers, извличащи задачи от една опашка, поддържа отворени точно толкова заявки, независимо от размера на партидата.

Стъпка 4: Следете реалното си потребление

Отворете Usage & Limits в dashboard. Броячите на живо до лимитите ви показват едновременността, честотата в минута и browser requests на ден в реално време, за да можете да оразмерите MAX_IN_FLIGHT спрямо реално изпълнение, а не на база предположения.

Activity Log записва и отказаните заявки. Изпълнение, което е приключило с точния брой редове, но с разпръснати rate_limit резултати, само е ограничило скоростта си.

Често срещани грешки

  • Изпращане на целия списък наведнъж. asyncio.gather върху 5000 URL адреса или Promise.all върху неограничен масив отваря 5000 заявки. Ограничете пула, а не списъка.
  • Повторни опити в паралел. Повторното изпращане на всяка отказана заявка в момента на отказа възпроизвежда пика, който го е причинил. Изчакайте Retry-After и добавете jitter.
  • Възприемане на дневен или периодичен лимит като кратко изчакване. 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 г.