Exécuter des requêtes en parallèle
Ce que vous allez apprendre
Comment exécuter un grand lot de requêtes FourA simultanément sans accumuler d'erreurs 429, en limitant le nombre d'appels ouverts et en réagissant à un refus au lieu de le répéter.
Prérequis
- Une clé API FourA (obtenez-en une ici)
- Python 3.9+ avec
requests, ou Node.js 18+
Les limites auxquelles vous faites face
Votre forfait applique deux plafonds par endpoint : le nombre de requêtes pouvant s'exécuter simultanément, et le nombre de requêtes pouvant démarrer par minute (Browser dispose d'un quota par jour au lieu d'un quota par minute). Single, Proxy et Browser ont chacun leurs propres quotas, et l'onglet Limits & Features de Usage & Limits les répertorie.
La requête qui dépasse le plafond de simultanéité renvoie un code HTTP 429 avec X-FourA-Limit: plan_limit_concurrency et le plafond dans le corps :
{
"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
}
Celle qui dépasse le plafond par minute renvoie X-FourA-Limit: plan_limit_rate, et retry_after_seconds indique le délai d'attente avant qu'une nouvelle requête ne soit acceptée (1 à 120 secondes).
Les deux refus sont immédiats. FourA ne met pas l'appel en file d'attente pour le traiter plus tard, donc rien n'est consommé ni facturé. Un appel refusé ne compte pas dans la minute glissante, ainsi un compte demandant légèrement plus que son plafond bénéficie toujours de la totalité de son quota. Une tempête de retries reçoit une réponse dédiée : lorsque les appels refusés continuent d'arriver à plus du double de votre plafond par minute, chaque appel sur cet endpoint est refusé avec cooldown: true et retry_after_seconds: 30, suspendez donc vos requêtes pendant ces 30 secondes. Référence complète des champs : Rate Limits.
Deux éléments importants sont souvent oubliés :
- Un appel
POST /api/auto/n'occupe aucun slot en lui-même, mais chaque sous-appel Single, Proxy et Browser qu'il exécute pour vous en occupe un. Un appel auto peut mobiliser plusieurs slots pendant le déroulement de sa stratégie. (Sur votre tableau de bord, l'appel auto correspond à la requête, et ses sous-appels correspondent à ses tentatives.) - Une requête Browser occupe son slot pendant toute la durée de rendu de la page, ce qui est bien plus long qu'une requête Single. Un lot d'appels browser atteint son plafond avec moins de requêtes qu'un lot d'appels single.
Étape 1 : Limitez votre propre concurrence
Choisissez une valeur inférieure au plafond de l'endpoint que vous ciblez et maintenez-la. Un pool de workers gère cela en une seule ligne :
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 constitue l'ensemble du mécanisme. Le pool n'a jamais plus d'appels ouverts que ce nombre, de sorte que le lot peut contenir un million d'URL tout en restant sous la limite.
Conservez une marge de sécurité. Si deux processus de votre côté utilisent le même compte, même avec des clés API différentes, ils partagent le même plafond, attribuez donc la moitié à chacun d'eux. Si une cible rapide permet au pool de terminer les requêtes plus vite qu'une minute ne l'autorise, cadencez les workers pour rester également sous le plafond par minute.
Étape 2 : Temporisez au lieu de renvoyer immédiatement
Si un refus survient, la mauvaise réponse consiste à renvoyer le lot immédiatement. Chaque appel qu'il contient sera de nouveau refusé, et les nouvelles tentatives s'accumuleront sur les requêtes déjà en cours d'exécution.
Attendez d'abord. La durée d'attente est indiquée dans le header Retry-After et dans retry_after_seconds dans le 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"}
Ajoutez du jitter lorsque vous exécutez de nombreux workers. Sans cela, chaque worker rejeté au cours de la même seconde se réveille au cours de la même seconde et subit un nouveau rejet simultané.
import random
time.sleep(wait + random.uniform(0, 0.5))
Étape 3 : La même chose avec 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;
}
Un nombre fixe de workers consommant une seule file d'attente maintient exactement ce nombre d'appels ouverts, quelle que soit la taille du lot.
Étape 4 : Observez votre consommation réelle
Ouvrez Usage & Limits dans le tableau de bord. Les compteurs en direct à côté de vos limites affichent la concurrence, le débit par minute et les requêtes de navigateur par jour en temps réel, ce qui vous permet de dimensionner MAX_IN_FLIGHT selon une exécution réelle plutôt qu'une estimation.
Le journal d'activité enregistre également les refus. Une exécution terminée avec le bon nombre de lignes mais une dispersion de résultats rate_limit s'est auto-régulée.
Erreurs fréquentes
- Lancer toute la liste d'un coup.
asyncio.gathersur 5 000 URL, ouPromise.allsur un tableau non borné, ouvre 5 000 appels. Limitez le pool, pas la liste. - Réessayer en parallèle. Renvoyer chaque appel refusé au moment même de son rejet reproduit le pic qui a causé le refus. Attendez le
Retry-Afteret ajoutez du jitter. - Traiter un quota journalier ou périodique comme une simple attente.
plan_limit_browser_dailyse réinitialise à minuit UTC,plan_limit_creditsetplan_limit_bandwidthà la fin de la période de facturation. Arrêtez l'exécution et lisezresets_atdans le corps de réponse lorsqu'il y en a un. - Compter les appels auto comme un seul slot chacun. L'escalade interne de
/api/auto/effectue de véritables sous-appels, et ce sont eux qui sont décomptés des plafonds. - Partager une clé entre plusieurs processus sans diviser le budget. Les plafonds s'appliquent par compte, pas par processus.
- Dimensionner un seul pool pour tous les endpoints. Single, Proxy et Browser ont chacun leur propre limite de concurrence. Un pool dimensionné pour Single dépassera le plafond inférieur de Browser.
Pages connexes
- Rate Limits : chaque format de refus et la signification de chaque champ
- Response Headers :
X-FourA-LimitetRetry-After - Request Outcomes : pourquoi un résultat
rate_limitn'est jamais facturé - Usage & Limits : où trouver les compteurs en direct et les chiffres de votre forfait
- Smart Fetch (Auto) : comment un appel auto génère plusieurs sous-appels