Reference

Limites de débit et limitation CaptchaAI

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_AVAILABLE et ERROR_TOO_MUCH_REQUESTS se rejouent, ERROR_WRONG_USER_KEY ou ERROR_ZERO_BALANCE doivent 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.

Guides connexes

Les commentaires sont désactivés pour cet article.