# Exécuter des requêtes en parallèle

> URL: https://foura.ai/fr/docs/how-to/run-requests-in-parallel
> Updated: 2026-10-15
> Index: https://foura.ai/llms.txt

## 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](https://foura.ai/docs/getting-started/authentication))
- 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](https://foura.ai/docs/dashboard/quota) 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 :

```json
{
  "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](https://foura.ai/docs/api/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 :

```python
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 :

```python
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é.

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

## Étape 3 : La même chose avec Node

```javascript
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](https://foura.ai/dashboard). 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é](https://foura.ai/docs/dashboard/activity-log) 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.gather` sur 5 000 URL, ou `Promise.all` sur 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-After` et ajoutez du jitter.
- **Traiter un quota journalier ou périodique comme une simple attente.** `plan_limit_browser_daily` se réinitialise à minuit UTC, `plan_limit_credits` et `plan_limit_bandwidth` à la fin de la période de facturation. Arrêtez l'exécution et lisez `resets_at` dans 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](https://foura.ai/docs/api/rate-limits) : chaque format de refus et la signification de chaque champ
- [Response Headers](https://foura.ai/docs/api/response-headers) : `X-FourA-Limit` et `Retry-After`
- [Request Outcomes](https://foura.ai/docs/api/outcomes) : pourquoi un résultat `rate_limit` n'est jamais facturé
- [Usage & Limits](https://foura.ai/docs/dashboard/quota) : où trouver les compteurs en direct et les chiffres de votre forfait
- [Smart Fetch (Auto)](https://foura.ai/docs/guides/smart-fetch) : comment un appel auto génère plusieurs sous-appels
