Huit mille CAPTCHAs par seconde, cela fait 28,8 millions par heure et 691 millions par jour. Un chiffre de cette taille est presque toujours cité comme une propriété du solveur, comme si la réponse tenait à un modèle plus rapide ou à une machine plus grosse. Ce n'est pas le cas. Bien avant que cela compte, 8 000 solves par seconde constituent un budget de concurrence, et la taille de ce budget se calcule au dos d'une enveloppe.
Le débit est égal à la concurrence divisée par le temps de service. Si un solve prend deux secondes et que vous voulez en terminer 8 000 chaque seconde, il vous faut 16 000 opérations ouvertes en permanence. Rien dans ce chiffre ne dépend de l'intelligence du solveur. Il découle de la latence que vous ne pouvez pas supprimer.
Cela déplace le vrai problème d'ingénierie. L'intérêt n'est pas le solve, c'est le control plane qui l'entoure. Quelque chose doit accepter le travail, le borner, maintenir des dizaines de milliers d'opérations en vol sans s'écrouler, et se mesurer suffisamment bien pour que vous puissiez nommer l'étape qui fixe la limite.
L'implémentation Go qui accompagne cet article, dans ScraperHub/how-we-solve-8000-captchas-per-second construit ce control plane en quatre fichiers : une file bornée, un pool de workers avec backpressure, une interface Solver interchangeable avec une implémentation mock et une implémentation Crawlbase, et un collecteur de métriques qui rapporte les solves par seconde ainsi que p50 et p99. Cet article la parcourt, exécute les deux benchmarks, puis fait la comptabilité honnête de ce que ces exécutions prouvent, et ne prouvent pas, à propos de 8 000.
- Le débit est la concurrence divisée par le temps de service. À deux secondes par solve, 8 000 par seconde signifient 16 000 requêtes en vol.
- Le control plane en Go n'est pas la contrainte. L'exécution mock tient 32 507 solves simulés par seconde sur une seule machine, à 0,6% de son propre plafond arithmétique.
- Le mock ne peut pas vous montrer une vraie queue de distribution. Son p99 est plafonné par construction à la latence de base plus le jitter.
- Entre 6 workers et 16 000 opérations en vol, trois choses cassent en premier : le pool de connexions HTTP par défaut, le slice de latences non borné, et l'idée qu'un processus constitue le déploiement.
- L'étape de solve est celle qu'il vaut mieux ne pas construire. C'est exactement ce que la Crawling API absorbe.
Solver , et chaque résultat converge vers un collecteur unique. La borne de la file est le mécanisme de backpressure.Le débit est la concurrence divisée par le temps de service
La loi de Little résume tout le plan de capacité en une ligne. Le nombre d'opérations en vol est égal au taux d'achèvement multiplié par la durée de chacune. Réarrangée pour la question qui nous occupe : le taux que vous pouvez soutenir est votre concurrence divisée par votre temps de service.
Cela vaut la peine d'être vérifié sur les chiffres du dépôt plutôt que pris pour argent comptant. L'exécution mock documentée utilise 256 workers et rapporte un p50 de 7,83 ms. Divisons : 256 workers pour 7,83 ms par solve prédisent 32 695 solves par seconde. La mesure donne 32 507. La file, le canal de résultats, le collecteur de fan-in et son mutex coûtent ensemble 0,57% du plafond théorique.
Faites le même calcul dans l'autre sens et le titre de cet article devient une commande de matériel.
| Taux visé | Temps de service | Concurrence requise | Ce que cela représente |
|---|---|---|---|
| 32 507/s | 7,83 ms (simulé) | 256 | 256 goroutines sur une machine |
| 8 000/s | 1,9 s (mesuré, chemin réel) | 15 200 | une flotte, pas un processus |
| 8 000/s | 2 s (chiffre rond) | 16 000 | même conclusion, calcul mental plus simple |
Les deux lignes vous disent où se situe réellement la difficulté. Le travail simulé est 250 fois plus rapide par élément qu'un aller-retour réseau réel, donc le benchmark local a besoin de trois ordres de grandeur de concurrence en moins pour afficher un chiffre plus élevé. Tout ce qui est difficile dans 8 000 solves réels par seconde, ce sont les 16 000 sockets ouverts, pas les 8 000 solves.
Quatre fichiers, une interface
Le module canonique exécutable est le répertoire final/ du dépôt compagnon. Il est volontairement petit.
pipeline.go bounded queue, worker pool, results fan-in solver.go the Solver interface and its two implementations metrics.go throughput and latency percentiles main.go load harness and flags config.go token and target URL from the environment
L'interface Solver est la couture qui rend le reste testable. La pipeline n'apprend jamais si un challenge est parti vers une simulation locale ou à travers Internet ; elle soumet du travail et enregistre ce qui revient. C'est ce qui permet de mesurer l'orchestration à 32 000 opérations par seconde sans réseau, puis de pointer la même pipeline vers un endpoint réel et de voir les chiffres s'effondrer pour des raisons que vous savez nommer.
Étape 1 : la file et le pool de workers
Un challenge est une structure et la file est un canal tamponné. La capacité du canal constitue tout le mécanisme de backpressure : dès que le tampon est plein, l'envoi du producteur bloque jusqu'à ce qu'un worker libère une place, si bien qu'un système surchargé ralentit son admission au lieu de faire grossir son tas.
Source : final/pipeline.go
func NewPipeline(workers, queueSize int, solver Solver, metrics *Metrics, target string) *Pipeline { return &Pipeline{ workers: workers, queue: make(chan Challenge, queueSize), // bounded => backpressure results: make(chan Result, queueSize), solver: solver, metrics: metrics, target: target, } }
Chaque worker est une goroutine qui itère sur ce canal. Itérer sur un canal est aussi le protocole d'arrêt : le producteur ferme la file quand il n'a plus de travail, les boucles vident ce qui reste puis sortent, et sync.WaitGroup indique à l'appelant quand le dernier a terminé.
func (p *Pipeline) worker(ctx context.Context, id int, wg *sync.WaitGroup) { defer wg.Done() for ch := range p.queue { started := time.Now() err := p.solver.Solve(ctx, ch) p.results <- Result{ ID: ch.ID, OK: err == nil, Latency: time.Since(started), Worker: id, } } }
Notez ce que le worker ne fait pas : il ne touche pas à la structure de métriques. Il rapporte un Result et passe au suivant. Une seule goroutine collectrice vide le canal de résultats dans le collecteur, si bien que les compteurs n'ont qu'un seul écrivain et que le chemin chaud reste un envoi sur canal.
Étape 2 : l'interface solveur et ses deux implémentations
La pipeline dépend de deux méthodes. C'est tout le contrat.
Source : final/solver.go
type Solver interface { Solve(ctx context.Context, c Challenge) error Name() string }
MockSolver tient la place de l'étape de solve avec une latence de base, un jitter uniforme et un taux d'échec, et ne fait aucun appel réseau. Il existe pour que vous puissiez mesurer l'orchestration isolément.
func (s *MockSolver) Solve(ctx context.Context, _ Challenge) error { d := s.Base if s.Jitter > 0 { d += time.Duration(rand.Int63n(int64(s.Jitter))) } select { case <-time.After(d): case <-ctx.Done(): return ctx.Err() } if s.FailRate > 0 && rand.Float64() < s.FailRate { return errors.New("mock solve failed") } return nil }
CrawlbaseSolver est le chemin réel. Il envoie l'URL cible à la Crawling API, où le traitement du CAPTCHA et de l'anti-bot a lieu pendant le fetch, et considère un 200 propre avec un corps entièrement lu comme une opération terminée. Vider vers io.Discard n'est pas cosmétique : un corps non lu ne peut pas être rendu au pool de connexions, ce qui va compter énormément dans un instant.
func (s *CrawlbaseSolver) Solve(ctx context.Context, c Challenge) error { endpoint := fmt.Sprintf( "https://api.crawlbase.com/?token=%s&url=%s", url.QueryEscape(s.Token), url.QueryEscape(c.URL), ) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return err } resp, err := s.Client.Do(req) if err != nil { return err } defer resp.Body.Close() _, _ = io.Copy(io.Discard, resp.Body) if resp.StatusCode != http.StatusOK { return fmt.Errorf("crawlbase status %d", resp.StatusCode) } return nil }
Même signature, physique radicalement différente. Le pool de workers ne peut pas les distinguer, et c'est tout l'intérêt.
Étape 3 : mesurer le débit et la queue ensemble
Un chiffre de débit seul masque les pannes intéressantes. Une pipeline peut afficher une moyenne saine alors qu'une fraction des requêtes prend un ordre de grandeur de plus que les autres, et dans un système concurrent ce sont précisément ces requêtes lentes qui retiennent votre budget de concurrence en otage. Le collecteur conserve donc chaque latence et en calcule les percentiles.
Source : final/metrics.go
func (m *Metrics) Report() Report { total := m.successes + m.failures elapsed := m.end.Sub(m.start) rate := 0.0 if elapsed > 0 { rate = float64(total) / elapsed.Seconds() } return Report{ Total: total, Successes: m.successes, Failures: m.failures, Elapsed: elapsed, SolvesPerSecond: rate, P50: m.percentile(50), P99: m.percentile(99), } }
Quatre signaux en sortent : le volume total, la répartition succès/échecs, p50 et p99. C'est cette paire qui transforme le réglage du nombre de workers en mesure plutôt qu'en supposition. Ajoutez des workers et observez les deux : si le taux monte pendant que p99 tient, la concurrence supplémentaire est absorbée. Si le taux plafonne pendant que p99 continue de grimper, vous accumulez une file en aval et ajouter des workers empire les choses.
Étape 4 : le harnais de charge
main.go câble les morceaux ensemble et met les molettes sur la ligne de commande, tandis que config.go lit le token et l'URL cible depuis l'environnement via un petit chargeur .env sans aucune dépendance.
Source : final/main.go
metrics := NewMetrics(*requests) pipeline := NewPipeline( *workers, *queueSize, solver, metrics, cfg.TargetURL, ) report := pipeline.Run( context.Background(), *requests, ) fmt.Println(report)
Les flags sont -solver, -requests, -workers, -queue, -base-ms, -jitter-ms, et -fail-rate. Les valeurs par défaut sont 20 000 requêtes, 256 workers, une file de 1 024, une latence de base de 5 ms avec 5 ms de jitter, et un taux d'échec de 1%.
Ce que l'exécution mock prouve, et ce qu'elle ne peut pas prouver
Mettez d'abord le control plane sous charge, sans réseau dans le chemin :
go run . -requests 20000 -workers 256 -queue 1024
solver=mock requests=20000 workers=256 queue=1024 target=https://example.com total=20000 ok=19804 fail=196 elapsed=615ms solves/sec=32507 p50=7.83ms p99=10.354ms
Trois éléments de cette sortie méritent une lecture attentive, car deux sont des confirmations et le troisième est une limite.
32 507 contre un plafond de 32 695. L'orchestration coûte 0,57%. Les envois sur canal, un mutex et un fan-in sur une seule goroutine ne sont pas là où vit un problème de débit, et vous avez maintenant le reçu plutôt que l'intuition.
196 échecs sur 20 000, soit 0,98%, face à un taux d'échec configuré à 1%. Le chemin d'erreur est donc bien parcouru et correctement compté. Passez -fail-rate 0 pour une exécution propre, mais un solveur qui n'échoue jamais n'est pas celui que vous mettez en production.
Un p99 de 10,354 ms n'est pas une queue de distribution. Le temps de service du mock est de 5 ms plus un jitter uniforme inférieur à 5 ms, donc 10 ms est un maximum arithmétique strict. Le p99 mesuré se situe 354 microsecondes au-dessus d'un plafond intégré à la simulation. C'est une mesure d'ordonnancement, pas une distribution de latence. Les vraies queues sont faites de DNS, de handshakes TLS, de retries, d'une origine lente et d'une IP malchanceuse, et rien de tout cela n'existe dans cette exécution. Croire un p99 issu d'un mock est la façon la plus simple d'être surpris en production.
Le benchmark mock est le bon premier pas et un mauvais dernier. Il prouve que la file et le pool savent comptabiliser des dizaines de milliers d'opérations par seconde sur une machine, ce qui est exactement ce que vous voulez savoir avant de les accuser. Il ne vous dit rien de l'étape qui prend réellement deux secondes.
Le chemin réel est borné par le réseau
Maintenant la même pipeline, le même code, contre un endpoint réel. Petit à dessein :
go run . -solver crawlbase -requests 12 -workers 6 -queue 32
solver=crawlbase requests=12 workers=6 queue=32 total=12 ok=12 fail=0 elapsed=3.38s solves/sec=4 p50=1.924091s p99=2.409689s
Quatre solves par seconde, et le chiffre rapporté est 3,55 arrondi par la chaîne de format. Douze échantillons, c'est beaucoup trop peu pour un percentile qui a du sens, traitez donc le p50 comme un ordre de grandeur : un solve par le chemin réel prend environ deux secondes, dont l'essentiel est le fetch et le travail anti-bot à l'autre bout, pas quoi que ce soit qui se passe en Go.
Ce seul chiffre est celui qui compte pour le dimensionnement, et c'est celui que le mock ne peut pas vous donner. Deux secondes de temps de service, voilà ce qui transforme 8 000 par seconde en 16 000 opérations concurrentes. La comparaison n'est pas mock 32 507 contre réel 4 ; c'est un control plane avec trois ordres de grandeur de marge posé devant une dépendance qui fixe le budget réel.
Trois choses cassent entre 6 workers et 16 000 opérations en vol
L'exemple tourne à 6 workers. La production tourne à quatre chiffres. Voici les trois murs que vous rencontrez en route, dans l'ordre où vous les rencontrez.
Le pool de connexions cède en premier
NewCrawlbaseSolver construit son client de la façon ordinaire :
Client: &http.Client{Timeout: 30 * time.Second}
L'absence de champ Transport signifie http.DefaultTransport, et http.DefaultTransport conserve deux connexions inactives par hôte. C'est DefaultMaxIdleConnsPerHost, et cette valeur est 2 depuis que net/http existe. À six workers contre un hôte d'API, personne ne le remarque. À deux mille workers contre un hôte d'API, toutes les connexions sauf deux sont démontées dès que la réponse est lue, si bien que presque chaque solve paie un nouveau handshake TCP et un nouveau handshake TLS avant de pouvoir envoyer un octet. Vous avez ajouté un aller-retour ou deux à une opération de deux secondes, brûlé du CPU en handshakes, et commencé à faire tourner les ports éphémères sans aucune raison.
Dimensionnez le transport pour la concurrence que vous voulez réellement :
transport := http.DefaultTransport.(*http.Transport).Clone() transport.MaxIdleConns = workers transport.MaxIdleConnsPerHost = workers // default is 2 transport.MaxConnsPerHost = workers // 0 means unlimited transport.IdleConnTimeout = 90 * time.Second client := &http.Client{Transport: transport, Timeout: 30 * time.Second}
Définir MaxConnsPerHost est aussi la façon de rendre la backpressure honnête de bout en bout. Sans cela, un pool de 16 000 workers essaiera volontiers d'ouvrir 16 000 sockets. Avec, les workers bloquent en attendant une connexion, ce qui est un mode de défaillance bien préférable à l'épuisement des descripteurs de fichiers. Relevez aussi la limite du processus, car la valeur par défaut du système d'exploitation est très loin de quatre chiffres de sockets simultanés.
Le slice de latences devient une fuite de mémoire
Le collecteur ajoute un time.Duration par solve et préalloue le slice au nombre de requêtes. Pour un benchmark de 20 000 requêtes cela fait 160 Ko et un tri que personne ne remarque. Pour un service, cela ne cesse jamais de grossir.
À 8 000 solves par seconde, huit octets par échantillon font 62,5 Ko par seconde, 230 Mo par heure et 5,5 Go par jour. Pire encore, percentile copie tout le slice et trie la copie à chaque appel, si bien qu'un rapport portant sur une heure de trafic trie 28,8 millions d'éléments et double l'empreinte mémoire pendant qu'il le fait. C'est le comportement correct pour un harnais de charge, dont le seul métier est de garder chaque échantillon d'une exécution bornée, et complètement faux pour tout ce qui vit longtemps.
Le correctif est le classique : un histogramme de latences de taille fixe avec des compteurs par intervalle, remis à zéro ou permuté à chaque fenêtre de reporting. Les percentiles deviennent une interpolation sur des compteurs au lieu d'un tri sur l'historique, la mémoire devient constante, et les chiffres cessent de vous mentir dès qu'une exécution survit à son allocation.
Un processus cesse d'être le déploiement
Seize mille requêtes en vol n'est pas un problème de nombre de goroutines. Les goroutines sont bon marché ; les sockets, les sessions TLS, les descripteurs de fichiers et la carte réseau devant eux ne le sont pas. Au-delà de quelques milliers de connexions simultanées vers une même destination, la forme doit changer : plusieurs instances de workers, une file partagée devant elles, des résultats qui coulent vers un stockage durable.
+----------------+ | shared queue | bounded, same as the channel +-------+--------+ | +--------------+--------------+ | | | v v v worker group worker group worker group | | | +--------------+--------------+ | v solve path (API) | v result stream
Le modèle survit au déménagement parce que rien en lui ne supposait un processus unique. Un canal Go devient une file partagée, une goroutine devient une instance de worker, le fan-in devient une pipeline de métriques, et queue to workers to solver to results se lit de la même façon aux deux échelles. C'est le véritable argument pour garder le control plane aussi petit : ce sont les mêmes quatre responsabilités que vous fassiez tourner 256 workers ou 8 instances de 2 000. Le même raisonnement est développé plus en détail dans notre article sur la construction d'un moteur de crawling distribué.
Le control plane est la partie qu'il vaut la peine d'écrire soi-même. L'étape de solve, non : le traitement du CAPTCHA et de l'anti-bot se fait à l'intérieur du fetch, derrière des IP résidentielles rotatives, et revient sous forme d'une réponse propre que votre worker compte ou réessaie. Pointez la même pipeline dessus et démarrez gratuitement avec 1 000 requêtes, sans carte bancaire.
La taille de la file et le nombre de workers ne font pas le même travail
Ces deux flags sont réglés ensemble et confondus en permanence.
Le nombre de workers fixe la concurrence. Il décide combien d'opérations de solve vous acceptez d'avoir ouvertes en même temps, ce qui, d'après la loi de Little, est le seul levier qui change votre taux soutenable.
La taille de la file fixe le tampon. Elle décide quel déséquilibre temporaire entre les arrivées et la capacité vous pouvez absorber avant que le producteur commence à bloquer.
Une file plus grande n'achète aucun débit. Si les workers terminent 1 000 opérations par seconde et que les producteurs en génèrent 2 000, la profondeur de la file choisit seulement combien de secondes s'écoulent avant qu'elle soit pleine. Elle se remplit, le producteur bloque, et la backpressure arrive exactement là où elle doit arriver. Dimensionner la file est en réalité une décision sur la tolérance aux rafales et sur l'âge que vous acceptez pour un élément en attente.
Réglez d'abord le mock, là où itérer est gratuit : -base-ms change le temps de service simulé, -jitter-ms ajoute de la variance, et -fail-rate vous laisse observer le chemin d'erreur sous charge. Ces trois molettes reproduisent la plupart des comportements qui vous intéressent avant qu'une seule requête réelle ne soit envoyée.
Ce qu'il faut emporter en production
Bornez le travail
Une file non bornée n'empêche pas la surcharge ; elle la cache jusqu'à ce que la mémoire devienne le mécanisme de défaillance, puis tombe d'un seul coup sans signal utile. Une file bornée convertit la même surcharge en backpressure, laquelle est visible, survivable et mesurable.
Gardez le solveur derrière une interface
La pipeline doit déplacer du travail, pas avoir d'opinion sur la manière dont ce travail est fait. Les deux implémentations ici sont l'argument : le même control plane a été mis sous charge à 32 507 opérations par seconde puis pointé vers une API réelle sans qu'une ligne change dans pipeline.go.
Mesurez l'étape, pas le système
Le débit et la latence de queue n'ont de sens qu'ensemble, et seulement quand vous savez quelle étape les a produits. Le pool de workers ne peut jamais exposer plus de débit utile que le solveur n'en soutient, donc un taux qui cesse de répondre aux workers ajoutés est une affirmation sur la dépendance. Nos notes sur la mise à l'échelle des projets de web scraping et cette étude de cas à 1 milliard de requêtes par mois reviennent toutes les deux à la même habitude : instrumenter étape par étape.
Exécuter le dépôt compagnon
Le dépôt exige Go 1.22 ou plus récent. Le chemin mock ne demande aucun compte ; le chemin Crawlbase demande un token.
git clone https://github.com/ScraperHub/how-we-solve-8000-captchas-per-second.git cd how-we-solve-8000-captchas-per-second/final cp .env.example .env # only needed for the crawlbase solver go build -o captcha-pipeline .
Deux variables d'environnement, toutes deux lues par config.go:
| Variable | Rôle |
|---|---|
CRAWLBASE_TOKEN |
Token pour le chemin -solver crawlbase . S'il manque, le programme affiche CRAWLBASE_TOKEN is required for the crawlbase solver et s'arrête. |
TARGET_URL |
URL que le solveur Crawlbase récupère pour chaque challenge. Par défaut https://example.com. |
final/ est le module canonique exécutable et steps/ contient des copies en lecture seule du fichier introduit à chaque étape ci-dessus, de sorte que vous pouvez lire la pipeline telle qu'elle était après l'étape 1 et pas seulement dans sa forme finale.
| Section | Chemin du code |
|---|---|
| Étape 1 : la file et le pool de workers | final/pipeline.go |
| Étape 2 : l'interface solveur et ses deux implémentations | final/solver.go |
| Étape 3 : mesurer le débit et la queue ensemble | final/metrics.go |
| Étape 4 : le harnais de charge |
final/main.go, final/config.go
|
Conclusion
Ce qu'il faut pour traiter 8 000 CAPTCHAs par seconde, c'est 16 000 opérations en vol, et toutes les difficultés découlent de ce seul chiffre plutôt que de la résolution elle-même.
Le control plane en Go est la moitié facile, et les mesures le disent : une file bornée, un pool de workers, une interface à deux méthodes et un collecteur de fan-in tournent à 0,6% de leur plafond arithmétique, à 32 507 opérations simulées par seconde. La moitié difficile consiste à maintenir ouvertes quatre chiffres de connexions réelles, à garder l'instrumentation à mémoire constante en le faisant, et à répartir l'ensemble sur plusieurs instances dès qu'une machine n'a plus de sockets.
Le résultat utile de cet exercice n'est donc pas un nombre de workers. Ce sont deux chiffres que vous pouvez défendre, le temps de service et la concurrence, et une pipeline suffisamment instrumentée pour dire quelle étape détient la limite. Avec eux, le taux visé devient une décision de capacité. Sans eux, il reste une supposition accompagnée d'un gros chiffre.
Foire aux questions
La commande d'exemple résout-elle 8 000 vrais CAPTCHAs par seconde ?
Non, et ce n'est pas son objet. La commande mock rapporte 32 507 solves simulés par seconde sans aucun réseau, et la commande Crawlbase du dépôt utilise une charge volontairement minuscule de 12 requêtes et rapporte environ 4 par seconde parce qu'elle est bornée par le réseau. Le chiffre de 8 000 décrit l'architecture à l'échelle de production, c'est-à-dire de nombreuses instances de workers devant une file partagée, pas un unique processus local. Ce que l'exemple vous donne, ce sont les deux entrées nécessaires pour dimensionner ce déploiement : la marge du control plane et le temps de service du chemin réel.
De combien de workers ai-je besoin pour 8 000 solves par seconde ?
Divisez le taux visé par le taux d'achèvement d'un seul worker. À environ deux secondes par solve, un worker termine 0,5 par seconde, donc 8 000 par seconde demandent environ 16 000 workers en vol. Que ce soit 8 instances de 2 000 ou 16 de 1 000 est une question de sockets, de descripteurs et de rayon d'impact, pas une question de Go. Mesurez votre propre p50 sur vos propres cibles avant de faire confiance au chiffre de deux secondes.
Pourquoi faire un benchmark avec un solveur mock ?
Pour découvrir si votre propre code est la contrainte avant d'accuser une dépendance. Le mock retire le réseau et simule le solve avec une latence, un jitter et un taux d'échec configurables, ce qui isole la file, le pool de workers et le fan-in. Ici, il a montré que l'orchestration coûte 0,57% de son plafond théorique : un déficit de débit dans l'exécution réelle n'est donc, de façon démontrable, pas dans le control plane.
Puis-je faire confiance au p99 d'une exécution mock ?
Non. Le temps de service du mock est une latence de base plus un jitter borné, donc son pire cas est fixé arithmétiquement : 5 ms plus au plus 5 ms donnent un plafond de 10 ms, et le p99 mesuré de 10,354 ms est ce plafond plus le coût d'ordonnancement. Une vraie queue vient du DNS, de TLS, des retries et d'origines lentes, et le mock ne simule rien de tout cela. Servez-vous des percentiles du mock pour détecter des problèmes d'ordonnancement dans votre propre code, et pour rien d'autre.
Pourquoi une file bornée plutôt qu'une file non bornée ?
Parce qu'une file non bornée ne supprime pas la surcharge, elle déplace simplement le symptôme vers la mémoire et retarde la panne jusqu'à ce qu'elle soit irrécupérable. Quand une file bornée se remplit, les producteurs attendent les workers, et c'est la backpressure : la pression remonte vers celui qui génère le travail, tandis que le travail en attente reste sous une limite que vous avez choisie délibérément.
Comment régler le nombre de workers ?
Augmentez -workers sur le chemin mock et observez le débit et p99 ensemble. Tant que le taux monte et que p99 reste plat, la concurrence ajoutée est absorbée. Quand le taux plafonne et que p99 continue de grimper, l'étape en aval est devenue la limite et des workers supplémentaires ne creusent qu'une file que vous ne voyez pas. Passez ensuite au solveur réel et recommencez, car les deux courbes n'ont pas du tout la même forme.
Que fait exactement le solveur Crawlbase ?
Il envoie l'URL cible à la Crawling API et considère un HTTP 200 propre avec un corps entièrement lu comme un solve terminé. Le travail sur le CAPTCHA et l'anti-bot se fait à l'intérieur de ce fetch et non dans votre processus, ce qui explique qu'un "solve" sur ce chemin soit une requête et une vérification de statut. Les approches du même problème côté appelant sont traitées dans notre guide sur le contournement des CAPTCHAs en web scraping.
Crawlez n'importe quel site à grande échelle, sans combattre l'infrastructure.
Crawlbase gère les proxies, les empreintes et les CAPTCHA afin que votre équipe livre des pipelines de données au lieu de maintenir la plomberie de crawl. 1 000 requêtes gratuites, sans carte requise.
