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

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

Как да изпълнявате голям пакет от 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, важащ до края на минутата.

И двата отказа са незабавни. FourA не поставя извикването на опашка, за да го върне по-късно, така че нищо не се изразходва и нищо не се таксува. Отказаните извиквания обаче се броят към плъзгащата се минута, така че поредица от повторни опити удължава собствения си период на изчакване. Пълна справка за полетата: Rate Limits.

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

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

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

Изберете число под лимита за съответния endpoint, който извиквате, и го поддържайте. Пул от workers прави това на един ред:

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: Изчакайте (Back Off), вместо да изпращате повторно

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

Първо изчакайте. Времето за изчакване е посочено в хедъра 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, черпещи от една опашка, поддържа точно толкова отворени повиквания, независимо от размера на batch-а.

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

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

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

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

  • Изпращане на целия списък наведнъж. asyncio.gather върху 5000 URL адреса или Promise.all върху неограничен масив отваря 5000 повиквания. Ограничете пула, а не списъка.
  • Паралелни повторни опити (retries). Повторното изпращане на всяко отказано повикване в момента на отказа му възпроизвежда пика, причинил отказа. Изчакайте Retry-After и добавете jitter.
  • Възприемане на дневния или периодния лимит като кратко изчакване. plan_limit_browser_daily се нулира в полунощ UTC, а plan_limit_credits и plan_limit_bandwidth в края на отчетния период. Спрете изпълнението и прочетете resets_at от тялото (body), когато има такъв.
  • Броене на auto повикванията като един слот всяко. Стъпките вътре в /api/auto/ правят реални подповиквания, и именно те се отчитат към лимитите.
  • Споделяне на ключ между процеси без разделяне на бюджета. Лимитите са за акаунт, а не за процес.
  • Оразмеряване на един пул за всеки endpoint. Single, Proxy и Browser имат свои собствени стойности за concurrency. Пул, оразмерен за Single, надвишава по-малкия таван за Browser.

Свързани

  • Rate Limits: Всеки тип отказ и значението на всяко поле
  • Response Headers: X-FourA-Limit и Retry-After
  • Request Outcomes: Защо резултат rate_limit никога не се таксува
  • Usage & Limits: Къде се намират броячите на живо и стойностите на вашия план
  • Smart Fetch (Auto): Как едно auto повикване се превръща в няколко подповиквания
Обновено: 10 септември 2026 г.