Aller au contenu principal

Sémaphore pour limiter les requêtes simultanées

Mis à jour le 30 juillet 2026

Le problème de la concurrence non contrôlée

Envoyer toutes vos requêtes en parallèle sans limitation est une erreur courante. Si vous avez 200 questions à traiter et que vous les lancez toutes simultanément, vous allez saturer les rate limits de l’API, recevoir des erreurs HTTP 429, et potentiellement voir votre clé API temporairement bloquée.

Sémaphore pour limiter les requêtes concurrentes

Le sémaphore est la solution standard pour contrôler le nombre de requêtes simultanées. C’est un compteur qui autorise un nombre maximal de coroutines à s’exécuter en parallèle.

5
Requêtes parallèles recommandées
429
Erreur HTTP rate limit
RPM
Requêtes par minute
TPM
Tokens par minute

Fonctionnement du sémaphore

asyncio.Semaphore agit comme un verrou à compteur. Quand une coroutine entre dans le bloc async with semaphore, le compteur diminue. Quand elle en sort, le compteur remonte. Si le compteur est à zéro, les coroutines suivantes attendent qu’une place se libère.

import asyncio
from asyncio import Semaphore

semaphore = Semaphore(5)  # maximum 5 requetes simultanees

async def process_request(request: str):
    async with semaphore:
        # Cette section est limitée a 5 exécutions parallèles
        chat = client.chat.create(
            model="grok-4.20-0309-reasoning",
            max_tokens=100
        )
        chat.append(user(request))
        return await chat.sample()

Dans cet exemple, même si vous lancez 100 coroutines avec asyncio.gather, seules 5 s’executeront simultanément. Les 95 autres attendront leur tour.

Pattern complet avec le SDK OpenAI

Voici le même pattern avec AsyncOpenAI, qui est souvent préféré pour sa compatibilité :

from openai import AsyncOpenAI
import httpx

client = AsyncOpenAI(
    api_key=os.getenv("XAI_API_KEY"),
    base_url="https://api.x.ai/v1",
    timeout=httpx.Timeout(3600.0)
)

async def send_request(sem: Semaphore, request: str) -> dict:
    async with sem:
        return await client.chat.completions.create(
            model="grok-4.20-0309-reasoning",
            messages=[{"role": "user", "content": request}]
        )

async def process_all(requests: list[str], max_concurrent: int = 5):
    sem = Semaphore(max_concurrent)
    tasks = [send_request(sem, r) for r in requests]
    return await asyncio.gather(*tasks)

Le paramètre max_concurrent est configurable. Vous le définissez en fonction de vos rate limits (leçon 7).

Choisir la bonne valeur de sémaphore

La valeur optimale se calcule à partir de vos rate limits, mais pas directement : elle passe par la durée des requêtes. Si votre tier autorise 60 RPM et que chaque requête dure environ 10 secondes, un sémaphore de 5 produit au maximum 5 requêtes simultanées, soit environ 30 requêtes par minute — confortablement sous la limite. C’est ce raisonnement (limite RPM × durée moyenne ÷ 60) qu’il faut refaire pour votre cas, et il explique pourquoi les modèles de raisonnement demandent un sémaphore plus bas : leurs requêtes durent plus longtemps, donc à concurrence égale, le débit par minute chute et le risque de saturation ponctuelle augmente. Dans le doute, la démarche empirique reste la plus sûre : commencez bas, entre 3 et 5, observez le taux d’erreurs 429, et montez progressivement jusqu’au point d’équilibre entre débit et stabilité.

Calcul approximatif

Requetes par minute = semaphore * (60 / duree_moyenne_secondes)

Exemple : semaphore=5, duree=10s → 5 * (60/10) = 30 RPM
Exemple : semaphore=10, duree=5s → 10 * (60/5) = 120 RPM

Erreur courante : sémaphore global vs local

# MAUVAIS : nouveau semaphore a chaque appel
async def bad_request(request: str):
    sem = Semaphore(5)  # chaque appel cree son propre semaphore
    async with sem:
        return await client.chat.completions.create(...)

# BON : semaphore partage entre tous les appels
sem = Semaphore(5)

async def good_request(request: str):
    async with sem:
        return await client.chat.completions.create(...)

Le sémaphore doit être partagé entre toutes les coroutines. S’il est créé localement dans chaque fonction, chaque coroutine aura son propre sémaphore et la limitation ne fonctionnera pas.

Points clés à retenir

  • Le sémaphore limite le nombre de requêtes API simultanées pour respecter les rate limits
  • Utilisez async with semaphore pour protéger les appels API
  • Commencez avec une valeur de 3 à 5 et ajustez selon vos limites RPM
  • Le sémaphore doit être partagé (global ou passe en paramètre), jamais crée localement
  • La formule RPM = semaphore * (60 / duree_moyenne) aide à dimensionner la valeur