CaptchaAI n'applique aucun quota de requêtes par seconde : rien ne vous empêche d'envoyer trente soumissions en une seconde sur in.php.
Ce qui vous limite, c'est le nombre de threads de votre plan et la disponibilité des workers. Quand cette capacité est pleine, l'API répond ERROR_NO_SLOT_AVAILABLE au lieu de mettre votre tâche en file d'attente.
La limitation de débit est donc votre travail, côté client. Un pipeline qui envoie tout d'un bloc ne résout pas plus vite ; il accumule des nouvelles tentatives et des tâches abandonnées.
Ce que l'API limite vraiment
| Facteur | Comportement |
|---|---|
| Taux de soumission | Aucun plafond strict par seconde |
| Tâches simultanées | Plus de 100 par compte |
| Fréquence de polling | Toutes les 5 s par tâche (recommandé) |
| Vérification du solde | Aucune limite |
L'unité de capacité n'est pas la requête, c'est le thread : un thread correspond à un CAPTCHA en cours de résolution et se libère dès que la résolution se termine.
La facturation suit ce modèle : un abonnement mensuel indexé sur les threads simultanés, résolutions illimitées par thread, sans plafond quotidien. BASIC ($15/mois, 5 threads) suffit à un script de test, ADVANCE ($90/mois, 50 threads) couvre un crawler de production.
Combien de threads pour quel volume
Le dimensionnement tient en une multiplication : threads nécessaires ≈ débit visé (résolutions par seconde) × temps de résolution médian (secondes).
Mesurez la latence sur vos formulaires réels avant de choisir un plan.
| Volume horaire | Concurrence visée | Approche |
|---|---|---|
| Moins de 100 | 1–5 | Séquentiel, aucun contrôle de débit |
| 100 à 1 000 | 5–20 | Concurrence bornée par un sémaphore |
| 1 000 à 10 000 | 20–50 | Asynchrone avec file d'attente et callbacks |
| Plus de 10 000 | 50–100 | Pool de workers et capacité dédiée |
Au-delà de 10 000 résolutions par heure, demandez une capacité dédiée au support CaptchaAI.
Un cas concret
Une équipe QA lyonnaise rejoue chaque nuit 4 000 parcours d'inscription depuis deux workers OVHcloud, sur une fenêtre de quatre heures. Soit 1 000 résolutions par heure, 0,28 par seconde : avec un temps de résolution médian mesuré à 14 s, quatre threads suffisent en régime permanent.
Mais la charge n'est pas lisse : les deux workers démarrent à la même minute, créant une pointe à 40 soumissions simultanées. C'est elle, pas la moyenne, qui déclenche ERROR_NO_SLOT_AVAILABLE ; un sémaphore et un décalage de démarrage la lissent.
Absorber ERROR_NO_SLOT_AVAILABLE sans casser le pipeline
Cette réponse n'est pas une erreur de configuration : la capacité est saturée à cet instant précis. Elle est transitoire, donc elle se rejoue — mais jamais dans une boucle serrée, qui aggraverait la congestion.
Le réflexe correct est un backoff exponentiel plafonné, avec les trois garde-fous ci-dessous.
Python
import time
import requests
API_KEY = "YOUR_API_KEY"
def submit_with_backoff(params, max_retries=5):
params["key"] = API_KEY
for attempt in range(max_retries):
resp = requests.get(
"https://ocr.captchaai.com/in.php", params=params
)
if resp.text.startswith("OK|"):
return resp.text.split("|")[1]
if resp.text == "ERROR_NO_SLOT_AVAILABLE":
wait = min(2 ** attempt * 2, 60) # 2, 4, 8, 16, 32s max 60
print(f"No slots, waiting {wait}s (attempt {attempt + 1})")
time.sleep(wait)
continue
raise Exception(f"Submit error: {resp.text}")
raise Exception("Max retries exceeded — no slots available")
Node.js
async function submitWithBackoff(params, maxRetries = 5) {
params.key = process.env.CAPTCHAAI_API_KEY;
for (let attempt = 0; attempt < maxRetries; attempt++) {
const resp = await axios.get("https://ocr.captchaai.com/in.php", {
params,
});
const text = String(resp.data);
if (text.startsWith("OK|")) {
return text.split("|")[1];
}
if (text === "ERROR_NO_SLOT_AVAILABLE") {
const wait = Math.min(2 ** attempt * 2000, 60000);
console.log(`No slots, waiting ${wait}ms (attempt ${attempt + 1})`);
await new Promise((r) => setTimeout(r, wait));
continue;
}
throw new Error(`Submit error: ${text}`);
}
throw new Error("Max retries exceeded");
}
- Plafonnez l'attente (60 s ici) : au-delà, échouez proprement et reprenez le lot plus tard.
- Ajoutez un jitter de quelques centaines de millisecondes, sinon vos workers repartent tous à la même seconde.
- Classez vos codes de retour :
ERROR_NO_SLOT_AVAILABLEetERROR_TOO_MUCH_REQUESTSse rejouent,ERROR_WRONG_USER_KEYouERROR_ZERO_BALANCEdoivent alerter une personne.
Limiter le débit côté client : token bucket ou sémaphore ?
| Outil | Ce qu'il borne | Réglé sur |
|---|---|---|
| Token bucket | La cadence d'envoi vers in.php |
La rafale que vous vous autorisez |
| Sémaphore | Les tâches en vol simultanément | Le nombre de threads de votre plan |
Token bucket en Python
import time
import threading
class RateLimiter:
def __init__(self, rate, per=1.0):
"""Allow `rate` requests per `per` seconds."""
self.rate = rate
self.per = per
self.tokens = rate
self.last_refill = time.monotonic()
self.lock = threading.Lock()
def acquire(self):
with self.lock:
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(self.rate, self.tokens + elapsed * (self.rate / self.per))
self.last_refill = now
if self.tokens >= 1:
self.tokens -= 1
return
else:
sleep_time = (1 - self.tokens) * (self.per / self.rate)
time.sleep(sleep_time)
self.acquire()
# Allow 10 submissions per second
limiter = RateLimiter(rate=10, per=1.0)
def submit_limited(params):
limiter.acquire()
return submit_with_backoff(params)
Sémaphore asyncio pour borner la concurrence
import asyncio
# Limit to 20 concurrent tasks
semaphore = asyncio.Semaphore(20)
async def solve_limited(solver, session, params):
async with semaphore:
return await solver.solve(session, params)
Confondre les deux est l'erreur la plus fréquente : le premier lisse les rafales, le second respecte les threads que vous payez.
Réglez le sémaphore avec une marge : sur 50 threads, une limite applicative à 45 laisse de la place aux tentatives en cours.
Régler la fréquence de polling
Interroger res.php toutes les secondes ne fait pas arriver le résultat plus tôt : vous multipliez les requêtes sans raccourcir la résolution.
La règle : une interrogation toutes les 5 secondes par tâche au plus, avec des intervalles qui s'allongent ensuite.
async def smart_poll(session, task_id, solver):
"""Polls with adaptive intervals."""
intervals = [5, 5, 5, 10, 10, 15, 15, 30, 30, 60]
for wait in intervals:
await asyncio.sleep(wait)
result = await solver.check(session, task_id)
if result is not None:
return result
raise TimeoutError(f"Task {task_id} timed out")
Tant que la tâche n'est pas prête, l'API renvoie CAPCHA_NOT_READY : c'est un statut, pas une erreur, et il ne doit pas entrer dans votre taux d'erreur.
Prévoyez aussi un timeout global : passé la dernière fenêtre, abandonnez la tâche et resoumettez-la.
Mesurer avant d'ajuster
Un limiteur réglé au jugé est soit trop permissif (erreurs en rafale), soit trop conservateur (threads payés et inutilisés).
Instrumentez d'abord : une fenêtre glissante de 60 secondes donne votre débit réel et votre taux d'erreur.
import time
from collections import deque
class APIMetrics:
def __init__(self, window=60):
self.window = window
self.requests = deque()
self.errors = deque()
def record_request(self):
now = time.time()
self.requests.append(now)
self._cleanup(self.requests, now)
def record_error(self, error_code):
now = time.time()
self.errors.append((now, error_code))
self._cleanup_tuples(self.errors, now)
def get_rate(self):
now = time.time()
self._cleanup(self.requests, now)
return len(self.requests) / self.window
def get_error_rate(self):
now = time.time()
self._cleanup(self.requests, now)
self._cleanup_tuples(self.errors, now)
if not self.requests:
return 0
return len(self.errors) / len(self.requests)
def _cleanup(self, dq, now):
while dq and dq[0] < now - self.window:
dq.popleft()
def _cleanup_tuples(self, dq, now):
while dq and dq[0][0] < now - self.window:
dq.popleft()
metrics = APIMetrics()
# Use in your submit function
def submit_tracked(params):
metrics.record_request()
try:
return submit_with_backoff(params)
except Exception as e:
metrics.record_error(str(e))
raise
# Check metrics periodically
print(f"Rate: {metrics.get_rate():.1f} req/s")
print(f"Error rate: {metrics.get_error_rate():.1%}")
| Indicateur | Lecture |
|---|---|
| Débit soutenu (req/s) | À comparer à la cadence du token bucket |
Part de ERROR_NO_SLOT_AVAILABLE |
Au-delà de quelques pour cent durables, revoyez le plan, pas le limiteur |
| Temps de résolution au 90e centile | Une dérive signale une saturation côté workers |
Côté journalisation, restez sobre : identifiant de tâche, code de retour, horodatage — jamais le contenu des formulaires.
Sur des parcours manipulant des données personnelles, cela évite un journal soumis au RGPD.
Dépannage
| Symptôme | Cause probable | Correctif |
|---|---|---|
Rafales de ERROR_NO_SLOT_AVAILABLE au démarrage |
Tous les workers envoient à la même seconde | Sémaphore global, démarrages décalés |
| Le taux d'erreur remonte à chaque vague de tentatives | Backoff sans jitter, les tentatives se resynchronisent | Délai aléatoire à chaque palier |
| Débit plafonné sous le nombre de threads payés | Token bucket réglé plus bas que la capacité réelle | Remonter la cadence par paliers |
ERROR_ZERO_BALANCE au milieu d'un lot |
Solde épuisé, pas un souci de débit | Vérifier le solde avant le lot |
FAQ
À quelle fréquence faut-il interroger res.php ?
Toutes les 5 secondes par tâche au plus vite, puis en espaçant : 5 s, 10 s, 15 s, 30 s. Plus souvent ne raccourcit pas la résolution.
Les nouvelles tentatives consomment-elles mon solde ?
Non. La facturation porte sur le nombre de threads simultanés, avec des résolutions illimitées par thread : une soumission refusée avec ERROR_NO_SLOT_AVAILABLE ne crée aucune tâche.
Passer à un plan avec plus de threads supprime-t-il l'erreur de capacité ?
Cela relève votre plafond de concurrence et réduit l'erreur quand elle vient de vos propres pointes.
La disponibilité des workers reste un facteur : gardez votre backoff.
Comment partager un quota de threads entre plusieurs machines ?
Si la charge est symétrique, divisez la limite par le nombre de processus : deux workers sur 50 threads, c'est 25 chacun. Sinon, centralisez le compteur dans un token bucket partagé (Redis).
Faut-il traiter les erreurs HTTP 429 et 5xx de la même façon ?
Oui, avec le même backoff exponentiel : ce sont des incidents transitoires. Les erreurs de paramètres, elles, se corrigent dans la requête.