Aucune page de tarifs ne vous dira comment un fournisseur se comporte sur vos formulaires. Envoyez quelques jours durant les mêmes défis CAPTCHA à votre fournisseur actuel et à CaptchaAI, puis comparez vos propres chiffres : c'est l'exécution parallèle. Voici le banc de test, les métriques et le plan de bascule.
Périmètre : ce banc s'exécute sur vos formulaires ou sur un environnement de recette dont vous avez la responsabilité.
Ce qu'une exécution parallèle prouve réellement
Vos CAPTCHA ne ressemblent à aucun benchmark public. Le sitekey, la géographie de vos proxys, l'heure de vos jobs, la part de reCAPTCHA v3 dans votre trafic : chacun de ces paramètres déplace les résultats. L'exécution parallèle ramène la décision à votre charge réelle, sans risque : le fournisseur historique reste en production.
Six mesures suffisent à trancher. Fixez-les avant la première ligne de code.
| Métrique | Comment la mesurer |
|---|---|
| Taux de réussite | résolutions_réussies / tentatives_totales × 100 |
| Temps de résolution médian | Délai entre l'envoi de la tâche et la réception du token |
| Temps de résolution P95 | Révèle les cas les plus lents |
| Répartition des codes d'erreur | Chaque code compté séparément, pas un total agrégé |
| Coût par résolution utile | Dépense mensuelle / résolutions acceptées |
| Validité du token | Le token est-il accepté par le formulaire, pas seulement renvoyé ? |
La dernière ligne est celle qu'on oublie : un token renvoyé en 8 s mais refusé vaut moins qu'un token renvoyé en 14 s et accepté.
Architecture du banc de test
Un routeur duplique chaque demande vers les deux fournisseurs et écrit les résultats dans le même collecteur.
┌──────────────┐
│ Your App │
└──────┬───────┘
│
┌──────▼───────┐
│ CAPTCHA │
│ Router │
└──┬───────┬───┘
│ │
┌────────▼──┐ ┌──▼────────┐
│ Current │ │ CaptchaAI │
│ Provider │ │ │
└────────┬──┘ └──┬────────┘
│ │
┌──▼───────▼──┐
│ Metrics │
│ Collector │
└─────────────┘
Mise en œuvre en Python
Une abstraction commune aux deux fournisseurs
Les deux services partagent le couple d'endpoints in.php / res.php : une classe, deux instanciations. Chaque résolution renvoie un objet mesuré — réussite, code d'erreur, durée — plutôt qu'une chaîne.
import os
import time
import requests
from dataclasses import dataclass, field
from typing import Optional
from concurrent.futures import ThreadPoolExecutor
@dataclass
class SolveResult:
provider: str
success: bool
solution: Optional[str] = None
error: Optional[str] = None
elapsed: float = 0.0
cost: float = 0.0
class CaptchaProvider:
def __init__(self, name, submit_url, result_url, api_key):
self.name = name
self.submit_url = submit_url
self.result_url = result_url
self.api_key = api_key
self.session = requests.Session()
def solve_recaptcha(self, sitekey, pageurl):
start = time.time()
resp = self.session.post(self.submit_url, data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
return SolveResult(
provider=self.name, success=False,
error=data.get("request"), elapsed=time.time() - start
)
captcha_id = data["request"]
for _ in range(60):
time.sleep(5)
result = self.session.get(self.result_url, params={
"key": self.api_key, "action": "get",
"id": captcha_id, "json": 1
}).json()
if result.get("status") == 1:
return SolveResult(
provider=self.name, success=True,
solution=result["request"], elapsed=time.time() - start
)
if result.get("request") != "CAPCHA_NOT_READY":
return SolveResult(
provider=self.name, success=False,
error=result.get("request"), elapsed=time.time() - start
)
return SolveResult(
provider=self.name, success=False,
error="TIMEOUT", elapsed=time.time() - start
)
Le runner qui lance les deux appels en même temps
Les deux résolutions partent dans la même seconde sur le même sitekey, sinon vous mesurez le site cible et non les fournisseurs. Un ThreadPoolExecutor à deux workers suffit.
class ParallelTestRunner:
def __init__(self, primary, challenger):
self.primary = primary
self.challenger = challenger
self.results = {"primary": [], "challenger": []}
def run_test(self, sitekey, pageurl, num_runs=20):
print(f"Running {num_runs} parallel solves...")
for i in range(num_runs):
with ThreadPoolExecutor(max_workers=2) as executor:
primary_future = executor.submit(
self.primary.solve_recaptcha, sitekey, pageurl
)
challenger_future = executor.submit(
self.challenger.solve_recaptcha, sitekey, pageurl
)
primary_result = primary_future.result()
challenger_result = challenger_future.result()
self.results["primary"].append(primary_result)
self.results["challenger"].append(challenger_result)
print(f" Run {i+1}/{num_runs}: "
f"{self.primary.name}={'OK' if primary_result.success else 'FAIL'} "
f"({primary_result.elapsed:.1f}s) | "
f"{self.challenger.name}={'OK' if challenger_result.success else 'FAIL'} "
f"({challenger_result.elapsed:.1f}s)")
return self.generate_report()
def generate_report(self):
report = {}
for label, results in self.results.items():
total = len(results)
successes = sum(1 for r in results if r.success)
times = [r.elapsed for r in results if r.success]
errors = [r.error for r in results if not r.success]
report[label] = {
"provider": results[0].provider if results else "unknown",
"total": total,
"successes": successes,
"success_rate": (successes / total * 100) if total else 0,
"avg_time": sum(times) / len(times) if times else 0,
"min_time": min(times) if times else 0,
"max_time": max(times) if times else 0,
"errors": errors
}
return report
# Usage
current = CaptchaProvider(
name="CurrentProvider",
submit_url="https://current-provider.com/in.php",
result_url="https://current-provider.com/res.php",
api_key="current_key"
)
captchaai = CaptchaProvider(
name="CaptchaAI",
submit_url="https://ocr.captchaai.com/in.php",
result_url="https://ocr.captchaai.com/res.php",
api_key=os.environ["CAPTCHAAI_API_KEY"]
)
runner = ParallelTestRunner(primary=current, challenger=captchaai)
report = runner.run_test(
sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl="https://example.com/form",
num_runs=20
)
for label, stats in report.items():
print(f"\n{stats['provider']}:")
print(f" Success rate: {stats['success_rate']:.1f}%")
print(f" Avg time: {stats['avg_time']:.1f}s")
print(f" Min/Max: {stats['min_time']:.1f}s / {stats['max_time']:.1f}s")
if stats['errors']:
print(f" Errors: {stats['errors']}")
Basculer une fraction du trafic réel
Une fois les mesures hors production rassurantes, envoyez une fraction du trafic vers le challenger, avec repli immédiat sur le fournisseur historique. Commencez à 10 %.
import random
class TrafficSplitter:
def __init__(self, primary, challenger, challenger_pct=10):
self.primary = primary
self.challenger = challenger
self.challenger_pct = challenger_pct
def solve(self, sitekey, pageurl):
if random.randint(1, 100) <= self.challenger_pct:
result = self.challenger.solve_recaptcha(sitekey, pageurl)
if not result.success:
# Fall back to primary on failure
return self.primary.solve_recaptcha(sitekey, pageurl)
return result
return self.primary.solve_recaptcha(sitekey, pageurl)
# Start with 10%, increase as confidence builds
splitter = TrafficSplitter(current, captchaai, challenger_pct=10)
result = splitter.solve(sitekey="...", pageurl="...")
La même boucle en Node.js
En Node.js, Promise.all remplace le pool de threads ; la logique ne bouge pas : envoi de la tâche, interrogation du résultat toutes les 5 s, arrêt sur code d'erreur ou sur timeout.
const axios = require("axios");
class CaptchaProvider {
constructor(name, submitUrl, resultUrl, apiKey) {
this.name = name;
this.submitUrl = submitUrl;
this.resultUrl = resultUrl;
this.apiKey = apiKey;
}
async solveRecaptcha(sitekey, pageurl) {
const start = Date.now();
try {
const submit = await axios.post(this.submitUrl, null, {
params: { key: this.apiKey, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
});
if (submit.data.status !== 1) {
return { provider: this.name, success: false, error: submit.data.request, elapsed: (Date.now() - start) / 1000 };
}
const captchaId = submit.data.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const poll = await axios.get(this.resultUrl, {
params: { key: this.apiKey, action: "get", id: captchaId, json: 1 },
});
if (poll.data.status === 1) {
return { provider: this.name, success: true, solution: poll.data.request, elapsed: (Date.now() - start) / 1000 };
}
if (poll.data.request !== "CAPCHA_NOT_READY") {
return { provider: this.name, success: false, error: poll.data.request, elapsed: (Date.now() - start) / 1000 };
}
}
return { provider: this.name, success: false, error: "TIMEOUT", elapsed: (Date.now() - start) / 1000 };
} catch (err) {
return { provider: this.name, success: false, error: err.message, elapsed: (Date.now() - start) / 1000 };
}
}
}
async function parallelTest(current, captchaai, sitekey, pageurl, runs = 20) {
const results = { current: [], captchaai: [] };
for (let i = 0; i < runs; i++) {
const [currentResult, captchaaiResult] = await Promise.all([
current.solveRecaptcha(sitekey, pageurl),
captchaai.solveRecaptcha(sitekey, pageurl),
]);
results.current.push(currentResult);
results.captchaai.push(captchaaiResult);
console.log(`Run ${i + 1}/${runs}: ${current.name}=${currentResult.success ? "OK" : "FAIL"} ` +
`(${currentResult.elapsed.toFixed(1)}s) | ${captchaai.name}=${captchaaiResult.success ? "OK" : "FAIL"} ` +
`(${captchaaiResult.elapsed.toFixed(1)}s)`);
}
for (const [label, data] of Object.entries(results)) {
const successes = data.filter((r) => r.success).length;
const times = data.filter((r) => r.success).map((r) => r.elapsed);
const avgTime = times.length ? times.reduce((a, b) => a + b, 0) / times.length : 0;
console.log(`\n${label}: ${successes}/${runs} success (${((successes / runs) * 100).toFixed(1)}%), avg ${avgTime.toFixed(1)}s`);
}
}
// Run
const currentProvider = new CaptchaProvider("CurrentProvider", "https://current-provider.com/in.php", "https://current-provider.com/res.php", "current_key");
const captchaai = new CaptchaProvider("CaptchaAI", "https://ocr.captchaai.com/in.php", "https://ocr.captchaai.com/res.php", process.env.CAPTCHAAI_API_KEY);
parallelTest(currentProvider, captchaai, "SITE_KEY", "https://example.com", 20);
Plan de bascule en quatre phases
- Critère de passage : taux de réussite stable sur 50 résolutions.
- Critère de passage : tokens acceptés par le formulaire, pas seulement renvoyés.
- Critère de blocage : repli automatique non testé.
| Phase | Durée | Répartition du trafic | Objectif |
|---|---|---|---|
| 1. Validation | 1 jour | 0 % en production | Vérifier la compatibilité des API |
| 2. Test d'ombre | 3 jours | 5 % vers CaptchaAI, avec repli | Collecter une base |
| 3. Montée en charge | 1 semaine | 25 % → 50 % → 75 % | Surveiller à chaque palier |
| 4. Bascule | — | 100 % vers CaptchaAI | Retirer l'ancien fournisseur |
Ne sautez pas la phase 1 : elle révèle les écarts de format de réponse et les paramètres que l'ancien fournisseur tolérait silencieusement.
Budgéter la période de double facturation
Pendant le test, vous payez deux fournisseurs : c'est le coût d'assurance de la migration, bien inférieur à celui d'une bascule ratée un lundi matin. La facturation CaptchaAI repose sur les threads simultanés et reste en dollars US.
- BASIC ($15/mois, 5 threads) : suffisant pour une campagne de 100 à 200 résolutions comparées.
- ADVANCE ($90/mois, 50 threads) : dimensionné pour une phase d'ombre à 25 % du trafic.
Si vos workers tournent chez OVHcloud, chez Scaleway ou sur la région AWS eu-west-3 (Paris), lancez le banc depuis ces machines, pas depuis votre poste : le Wi-Fi ajoute 1 à 2 s de latence, attribuées à tort au fournisseur. Côté RGPD, les logs du banc gardent des identifiants de tâche et des durées, jamais les données personnelles saisies dans le formulaire testé.
Dépannage
| Problème | Cause | Correctif |
|---|---|---|
| Un fournisseur paraît systématiquement plus lent | Latence réseau du poste de test | Relancez le banc depuis vos serveurs de production |
| Les taux divergent d'un jour à l'autre | Échantillon trop faible | Passez à plus de 50 résolutions par fournisseur, étalées sur la journée |
| Le token est renvoyé mais refusé | Token expiré avant soumission | Injectez le token dès sa réception, sans mise en file d'attente |
| La facture double pendant le test | Chaque défi est résolu deux fois | Limitez la phase parallèle à un échantillon du trafic |
Beaucoup de CAPCHA_NOT_READY |
Intervalle d'interrogation trop court | Gardez 5 s entre deux appels à res.php, plus un timeout global |
Horodatez chaque incident : la répartition des causes en dit plus long que la moyenne des temps de résolution.
FAQ
Combien de temps faut-il garder les deux fournisseurs en parallèle ?
Une dizaine de jours : un jour de validation, trois jours d'ombre, une semaine de montée en charge. Au-delà, vous payez deux abonnements sans rien apprendre de plus.
À quel moment de la journée faut-il mesurer ?
À plusieurs moments : le matin, en début d'après-midi et la nuit. La charge des files varie selon les fuseaux, et une moyenne calculée sur une seule plage horaire est trompeuse.
Faut-il tester avec vos proxys réels ?
Oui, si vous en utilisez en production. La qualité d'un proxy résidentiel influence l'acceptation du token ; un banc sans proxy mesure un scénario que vous n'exploiterez jamais.
Et si les deux fournisseurs échouent sur le même type de CAPTCHA ?
Vérifiez la couverture annoncée avant d'incriminer les taux. CaptchaAI prend en charge reCAPTCHA v2 et v3, Cloudflare Turnstile et Cloudflare Challenge, GeeTest v3, les CAPTCHA image/OCR et texte et les grilles d'images ; hCaptcha et FunCaptcha ne sont pas pris en charge, GeeTest v4 est annoncé comme à venir, et CaptchaFox (bêta), Friendly Captcha (bêta) et Lemin (bêta) restent en bêta. Pondérez vos résultats selon la part réelle de chaque type dans votre trafic.
Peut-on automatiser ce banc dans la CI ?
Oui : une série courte planifiée chaque nuit détecte une dérive du taux de réussite bien avant vos utilisateurs.
Articles connexes
- Construire un pipeline de tests automatisés
- Exécuter vos tests CAPTCHA dans GitHub Actions
- Gérer les CAPTCHA dans vos tests Playwright
Prochaines étapes
Le test parallèle ne coûte qu'un peu de temps machine : créez votre compte CaptchaAI et laissez vos chiffres décider.
Guides associés :