Semaphore pour limiter les requetes simultanees
Le probleme de la concurrence non controlee
Envoyer toutes vos requetes en parallele sans limitation est une erreur courante. Si vous avez 200 questions a traiter et que vous les lancez toutes simultanement, vous allez saturer les rate limits de l’API, recevoir des erreurs HTTP 429, et potentiellement voir votre cle API temporairement bloquee.

Le semaphore est la solution standard pour controler le nombre de requetes simultanees. C’est un compteur qui autorise un nombre maximal de coroutines a s’executer en parallele.
Fonctionnement du semaphore
asyncio.Semaphore agit comme un verrou a 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 a zero, les coroutines suivantes attendent qu’une place se libere.
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 limitee a 5 executions paralleles
chat = client.chat.create(
model="grok-4.20-reasoning",
max_tokens=100
)
chat.append(user(request))
return await chat.sample()
Dans cet exemple, meme si vous lancez 100 coroutines avec asyncio.gather, seules 5 s’executeront simultanement. Les 95 autres attendront leur tour.
Pattern complet avec le SDK OpenAI
Voici le meme pattern avec AsyncOpenAI, qui est souvent prefere pour sa compatibilite :
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-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 parametre max_concurrent est configurable. Vous le definissez en fonction de vos rate limits (lecon 7).
Choisir la bonne valeur de semaphore
La valeur optimale du semaphore depend de trois facteurs :
- Vos rate limits RPM : si votre tier autorise 60 RPM (requetes par minute), un semaphore de 5 genere au maximum 5 requetes simultanees. Si chaque requete dure 10 secondes, vous envoyez environ 30 requetes par minute, ce qui reste sous la limite.
- La duree moyenne des requetes : les modeles de raisonnement prennent plus de temps. Ajustez le semaphore a la baisse pour ces modeles.
- Votre tolerance aux erreurs : commencez bas (3-5) et augmentez progressivement jusqu’a trouver le bon equilibre.
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 : semaphore 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 semaphore doit etre partage entre toutes les coroutines. S’il est cree localement dans chaque fonction, chaque coroutine aura son propre semaphore et la limitation ne fonctionnera pas.
Points cles a retenir
- Le semaphore limite le nombre de requetes API simultanees pour respecter les rate limits
- Utilisez
async with semaphorepour proteger les appels API - Commencez avec une valeur de 3 a 5 et ajustez selon vos limites RPM
- Le semaphore doit etre partage (global ou passe en parametre), jamais cree localement
- La formule
RPM = semaphore * (60 / duree_moyenne)aide a dimensionner la valeur