Exécuter des requêtes en parallèle
Ce que vous allez apprendre
Comment exécuter simultanément un volume important de requests FourA sans accumuler d'erreurs 429, en limitant le nombre d'appels ouverts et en réagissant à un refus plutôt qu'en le répétant.
Prérequis
- Une clé API FourA (obtenez-en une ici)
- Python 3.9+ avec
requests, ou Node.js 18+
Les limites applicables
Votre forfait applique deux plafonds par endpoint : le nombre de requests exécutables simultanément, et le nombre 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 détaille.
La request qui dépasse le plafond de concurrence renvoie une erreur HTTP 429 avec X-FourA-Limit: plan_limit_concurrency et le plafond dans le corps de réponse :
{
"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 avec retry_after_seconds courant jusqu'à la fin de la minute.
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 dépensé et rien n'est facturé. Les appels refusés sont toutefois comptabilisés dans la minute glissante, de sorte qu'une tempête de retries prolonge son propre temps d'attente. Référence complète des champs : Rate Limits.
Deux éléments importants sont souvent ignorés :
- Un appel
POST /api/auto/n'est pas comptabilisé en lui-même, mais chaque sous-appel Single, Proxy et Browser qu'il effectue pour vous l'est. Un seul appel auto peut occuper plusieurs slots pendant l'exécution de sa cascade. - Une requête Browser occupe son slot pendant toute la durée du 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 un nombre inférieur au plafond de l'endpoint que vous appelez et maintenez-le. Un pool de workers effectue 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 cette limite, le lot peut donc contenir un million d'URL tout en restant sous le plafond.
Conservez une marge de sécurité. Si deux processus de votre côté partagent une même clé API, ils partagent un même plafond : attribuez donc la moitié à chacun. Si une cible rapide permet au pool de terminer les requêtes plus vite qu'une minute ne l'autorise, régulez le rythme des workers pour rester également sous le plafond par minute.
Étape 2 : Temporiser au lieu de renvoyer
En cas de refus, la mauvaise approche consiste à renvoyer immédiatement le lot. 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 l'en-tête Retry-After et dans retry_after_seconds au sein du corps :
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 à 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 file d'attente unique 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 sur la base d'une exécution réelle plutôt que d'une estimation.
Le Journal d'activité enregistre également les refus. Une exécution qui s'est terminée avec le bon nombre de lignes mais une dispersion de résultats rate_limit s'est auto-limitée.
Erreurs fréquentes
- Lancer toute la liste d'un coup.
asyncio.gathersur 5 000 URLs, ouPromise.allsur un tableau non limité, ouvre 5 000 appels. Limitez le pool, pas la liste. - Réessayer en parallèle. Renvoyer chaque appel refusé dès qu'il est refusé reproduit le pic qui a causé le refus. Attendez le
Retry-Afteret ajoutez un délai aléatoire (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 lorsqu'il est présent. - Compter les appels auto comme un seul emplacement chacun. L'escalade interne de
/api/auto/effectue de réels sous-appels, et ce sont eux que les plafonds comptabilisent. - Partager une clé entre plusieurs processus sans diviser le budget. Les plafonds sont par compte, pas par processus.
- Dimensionner un pool unique pour tous les endpoints. Single, Proxy et Browser ont chacun leur propre limite de concurrence. Un pool dimensionné pour Single dépasse le plafond plus restreint de Browser.
Ressources associées
- Limites de débit (Rate Limits) : Chaque format de refus et la signification de chaque champ
- En-têtes de réponse :
X-FourA-LimitetRetry-After - Résultats des requêtes : 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 se transforme en plusieurs sous-appels