Wykonywanie równoległych requestów

Czego się dowiesz

Jak uruchomić dużą partię żądań FourA współbieżnie bez otrzymywania błędów 429, poprzez ograniczenie liczby otwartych wywołań oraz reagowanie na odrzucenie zamiast jego natychmiastowego ponawiania.

Wymagania wstępne

Limity, z którymi pracujesz

Twój plan obejmuje dwa limity dla każdego endpointu: ile żądań może być przetwarzanych jednocześnie oraz ile może zostać uruchomionych w ciągu minuty (Browser ma limit dzienny zamiast minutowego). Single, Proxy i Browser mają własne wartości, a zakładka Limits & Features w sekcji Usage & Limits zawiera ich listę.

Żądanie, które przekroczy limit współbieżności, zwraca kod HTTP 429 z X-FourA-Limit: plan_limit_concurrency oraz wartością limitu w treści odpowiedzi:

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

To, które przekracza limit minutowy, zwraca X-FourA-Limit: plan_limit_rate oraz retry_after_seconds trwający do końca minuty.

Obie odmowy następują natychmiastowo. FourA nie kolejkuje wywołania i nie przetwarza go później, więc nic nie zostaje zużyte ani naliczone. Odrzucone wywołania wliczają się jednak do ruchomego okna minutowego, więc gwałtowna seria ponowień wydłuża czas oczekiwania. Pełny opis pól: Rate Limits.

Dwie kwestie, o których często się zapomina:

  • Wywołanie POST /api/auto/ nie jest liczone samo w sobie, ale liczone jest każde podwywołanie Single, Proxy i Browser wykonane w ramach niego. Jedno wywołanie auto może zajmować więcej niż jeden slot podczas działania drabinki.
  • Żądanie Browser zajmuje swój slot tak długo, jak trwa renderowanie strony, co trwa znacznie dłużej niż żądanie Single. Pakiet wywołań browser osiąga limit przy mniejszej liczbie żądań niż pakiet wywołań single.

Krok 1: Ogranicz współbieżność po swojej stronie

Wybierz wartość poniżej limitu dla wywoływanego endpointu i jej nie przekraczaj. Pula workerów realizuje to w jednej linii:

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 to cały mechanizm. Pula nigdy nie ma więcej otwartych wywołań niż ta liczba, więc wsad może mieć nawet milion adresów URL i nadal mieścić się w limicie.

Zostaw margines. Jeśli dwa procesy po Twojej stronie dzielą jeden klucz API, współdzielą też jeden limit, więc przydziel każdemu z nich połowę. Jeśli szybki cel pozwala puli kończyć żądania szybciej niż pozwala na to minuta, dostosuj tempo workerów, aby nie przekroczyć również limitu na minutę.

Krok 2: Wstrzymaj operacje zamiast ponawiać wysyłkę

Jeśli nadejdzie odmowa, złym rozwiązaniem jest natychmiastowe ponowne wysłanie całego wsadu. Każde wywołanie w nim zostanie ponownie odrzucone, a ponowienia nałożą się na żądania, które już były w toku.

Najpierw odczekaj. Czas oczekiwania znajduje się w nagłówku Retry-After oraz w polu retry_after_seconds w treści odpowiedzi:

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

Dodaj jitter, gdy uruchamiasz wiele procesów roboczych. Bez tego każdy worker odrzucony w tej samej sekundzie budzi się w tej samej sekundzie i ponownie zostaje odrzucony razem z innymi.

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

Krok 3: To samo w 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;
}

Stała liczba workerów pobierających zadania z jednej kolejki utrzymuje dokładnie tyle otwartych wywołań, niezależnie od wielkości partii.

Krok 4: Sprawdź rzeczywiste użycie

Otwórz Usage & Limits w dashboardzie. Liczniki na żywo obok Twoich limitów pokazują współbieżność, liczbę żądań na minutę oraz liczbę żądań przeglądarki na dzień w czasie rzeczywistym, co pozwala dobrać MAX_IN_FLIGHT na podstawie faktycznego przebiegu, a nie domysłów.

Activity Log rejestruje również odrzucenia. Przebieg, który zakończył się poprawną liczbą wierszy, ale zawierał rozproszone wyniki rate_limit, samoczynnie podlegał dławieniu (throttling).

Częste błędy

  • Wysyłanie całej listy naraz. asyncio.gather na 5000 adresach URL lub Promise.all na nieograniczonej tablicy otwiera 5000 wywołań. Ogranicz pulę, a nie listę.
  • Równoległe ponawianie prób. Ponowne wysłanie każdego odrzuconego wywołania w momencie jego odrzucenia odtwarza skok ruchu, który spowodował problem. Odczekaj Retry-After i dodaj losowe opóźnienie (jitter).
  • Traktowanie limitu dziennego lub okresowego jako zwykłego oczekiwania. plan_limit_browser_daily resetuje się o północy UTC, a plan_limit_credits oraz plan_limit_bandwidth na koniec okresu rozliczeniowego. Zatrzymaj przebieg i odczytaj resets_at z treści odpowiedzi, jeśli występuje.
  • Liczenie wywołań auto jako jednego slotu. Mechanizm wewnątrz /api/auto/ wykonuje rzeczywiste podwywołania i to one wliczają się do limitów.
  • Współdzielenie klucza między procesami bez podziału budżetu. Limity są przypisane do konta, a nie do pojedynczego procesu.
  • Ustawianie jednej puli dla każdego endpointu. Single, Proxy i Browser mają własne limity współbieżności. Pula zwymiarowana pod Single przekroczy mniejszy limit dla Browser.

Powiązane

Aktualizacja: 10 września 2026