Quelqu'un finit par poser la question d'infrastructure : que faut-il pour tenir 10 000 sessions de navigateur simultanées ? Cela ressemble à une question de capacité appelant une réponse matérielle. C'est en réalité une question d'unités, et les unités sont fausses.
Dix mille sessions simultanées ne sont pas une charge. C'est une décision de provisionnement. La charge, ce sont des pages par jour, et les deux sont reliées par une seule ligne d'arithmétique qui tranche tout le débat construire ou acheter avant qu'un seul serveur ne soit chiffré.
Cet article construit ce qui gouverne une flotte de navigateurs, en TypeScript et Playwright : un pool de sessions avec un plafond de concurrence strict, du leasing et du recyclage. Il chiffre ensuite la flotte que 10 000 sessions exigent réellement, convertit ce nombre en la charge qu'il sert, et le compare au rendu managé à débit égal plutôt qu'à chiffre d'affiche égal.
- La concurrence est le débit multiplié par le temps de service. À cinq secondes par page rendue, 10 000 sessions font 2 000 pages par seconde, soit 172,8 millions par jour.
- La plupart des équipes qui demandent 10 000 sessions en ont besoin d'environ 1%. Un million de pages par jour demande une concurrence d'à peu près 58.
- La flotte qui soutient 10 000 sessions, c'est environ 100 nœuds et 3,2 To de RAM, dimensionnés pour le pic et inactifs le reste du temps.
- Une page bloquée coûte à une flotte auto-hébergée les mêmes RAM-secondes qu'une page réussie. Sur la Crawling API, les requêtes en échec ne sont pas facturées du tout.
- Chaque contexte d'un nœud vit à l'intérieur d'un seul processus navigateur. Un plantage les emporte tous.
10 000 sessions est un chiffre de provisionnement, pas une charge
La loi de Little est la conversion. Le nombre d'opérations en vol est égal au taux d'achèvement multiplié par la durée de chacune. Pour une flotte de navigateurs, cela se lit : la concurrence est le nombre de pages par seconde multiplié par les secondes par page.
Une page lourde en JavaScript rendue jusqu'à domcontentloaded prend quelques secondes. Disons cinq. Alors 10 000 sessions simultanées ne sont pas du tout un chiffre de demande, mais ceci :
| Charge | Pages par seconde | Concurrence nécessaire à 5 s par page |
|---|---|---|
| 1 million de pages/jour | 11,6 | 58 |
| 10 millions de pages/jour | 115,7 | 579 |
| 172,8 millions de pages/jour | 2 000 | 10 000 |
Dix mille sessions simultanées, c'est le provisionnement pour 172,8 millions de pages par jour. Si votre besoin réel est d'un million de pages par jour, la concurrence qui le sert est de 58, et la demande de 10 000 sessions est environ 173 fois plus grande que la charge qui la motive.
Cela vaut la peine d'être tranché avant de chiffrer quoi que ce soit, car toute la comparaison construire ou acheter change selon le chiffre qui est réel. La version honnête de la question n'est donc pas "pouvons-nous faire tourner 10 000 sessions" mais "quelles sont nos pages par jour, quelle est notre latence par page, et quelle concurrence ce couple implique-t-il ?"
L'implémentation de référence
Le projet compagnon dans ScraperHub/scaling-a-headless-browser-fleet-to-10000-concurrent-sessions est organisé en trois pièces exécutables, chacune répondant à une partie différente de la question.
session.ts one browser process, many isolated contexts pool.ts the control plane: ceiling, leasing, recycling capacity.ts per-session assumptions to node count and RAM crawlbase.ts the managed path, one HTTP request index.ts the runner for all three modes
Il faut Node.js 18 ou plus récent et le Chromium de Playwright. Seul le chemin managé demande un token.
git clone https://github.com/ScraperHub/scaling-a-headless-browser-fleet-to-10000-concurrent-sessions.git cd scaling-a-headless-browser-fleet-to-10000-concurrent-sessions/final npm install npx playwright install chromium cp .env.example .env
Une session est un contexte, pas un navigateur
La première décision d'architecture est l'unité de concurrence. Ici une session est un contexte de navigateur Playwright, pas un processus navigateur. Un contexte porte ses propres cookies, son propre stockage et son propre état d'exécution, pour une fraction du coût d'un processus Chromium neuf. C'est précisément ce qui rend la haute densité possible.
Source : final/src/session.ts
async open(url: string, timeoutMs: number): Promise<OpenResult> { const page = await this.context.newPage(); try { const response = await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' }); const title = await page.title(); return { status: response ? response.status() : 0, title }; } finally { await page.close(); } }
La page est volontairement éphémère : ouvrir, naviguer, lire un signal, fermer. Des durées de vie courtes empêchent l'état de s'accumuler, si bien qu'un seul contexte sert de nombreuses tâches successives pendant que le cycle de vie du navigateur reste dans la factory.
La factory lance un Chromium et crée chaque contexte à l'intérieur. C'est ce qui rend un contexte bon marché, et cela signifie aussi qu'un plantage du navigateur emporte toutes les sessions de ce nœud. À un plafond opérationnel de 100 sessions par nœud, un plantage coûte 100 sessions, et le contrôle healthy() du pool le découvre session par session, après coup, au prochain acquire ou release. La densité et le rayon d'impact sont le même curseur.
Le control plane est l'endroit où vit la garantie
Le pool décide si un appelant reçoit une session existante, une nouvelle, ou une attente. Ces trois issues constituent toute la stabilité de la flotte.
Source : final/src/pool.ts
if (this.live < this.maxConcurrency) { // Reserve the slot synchronously, before the await. this.live += 1; this.created += 1; this.inUse += 1; this.peakInUse = Math.max(this.peakInUse, this.inUse); try { return await this.factory.create(); } catch (error) { this.live -= 1; this.inUse -= 1; throw error; } } // Fleet is full. Queue and wait for a release. return new Promise<Session>((resolve) => { this.waiters.push(resolve); });
Un détail porte toute la garantie : le créneau est réservé avant l'attente de factory.create(). Parce que await rend la main, plusieurs appelants liraient sinon tous live < maxConcurrency pendant que le premier contexte est encore en création. Un plafond de cinq pourrait brièvement devenir vingt. Réserver de façon synchrone et annuler en cas d'échec est ce qui fait de la limite une vraie limite plutôt qu'un conseil.
Le chemin de release boucle la boucle. Une session trop vieille ou qui échoue à son contrôle de santé est recyclée, et lorsque des appelants attendent déjà, le pool crée immédiatement un remplaçant, pour que la capacité ne rétrécisse pas silencieusement à chaque mise à la retraite.
À quoi cela ressemble sous contention
Lancez plus de tâches que le plafond n'en autorise :
npm run pool
tasks=20 ok=20 elapsed=541ms throughput=37.0/s pool: maxConcurrency=5 peakInUse=5 created=5 recycled=0 peak utilization=100%
La deuxième ligne est celle qui compte. Vingt tâches terminées, cinq contextes existants, et peakInUse n'a jamais dépassé le plafond. Les quinze autres tâches ont été servies en prêtant et rendant les mêmes cinq sessions. Mettez MAX_SESSION_AGE_MS=0 et c'est le compteur recycled qui grimpe, ce qui exerce le chemin de mise à la retraite qui empêche les flottes de longue durée d'accumuler un état périmé.
Notez ce que cette exécution n'établit pas . Ce sont cinq contextes contre example.com sur une seule machine : cela valide la logique d'allocation et rien sur la mise à l'échelle. Le chiffre de débit, 37 requêtes par seconde, est une propriété d'une page triviale et d'un navigateur local, pas une prévision.
Chiffrer la flotte
Le modèle de capacité transforme des hypothèses par session en un nombre de nœuds et une facture de RAM. Il est volontairement petit, car l'important est l'arithmétique, pas l'outil.
Source : final/src/capacity.ts
const usableRamMb = (input.nodeRamGb - input.nodeReserveGb) * 1024; const ramBoundSessionsPerNode = Math.max( 1, Math.floor(usableRamMb / input.ramPerSessionMb) ); const effectiveSessionsPerNode = Math.min( input.configuredSessionsPerNode, ramBoundSessionsPerNode ); const nodes = Math.ceil(input.targetSessions / effectiveSessionsPerNode);
Avec 250 Mo par session, des nœuds de 32 Go qui en retiennent 4 chacun, et un plafond opérationnel de 100 sessions par nœud :
target sessions: 10000 RAM-bound sessions/node: 114 effective sessions/node: 100 nodes required: 100 total fleet RAM: ~3200 GB
La mémoire seule autoriserait 114 sessions par nœud. Le modèle retient 100 parce que c'est le nombre que vous feriez réellement tourner, et l'écart entre les deux est la différence entre une fiche technique et une flotte de production. Le résultat est d'environ 100 nœuds et 3,2 To de RAM, avant l'autoscaling, la maintenance des images de navigateur, la reprise sur plantage, la supervision, les déploiements et l'astreinte qui maintient tout cela en vie.
Deux coûts que le nombre de nœuds ne montre pas
Le chiffre de 100 nœuds est le prix affiché. Deux choses déplacent le vrai, et toutes deux favorisent le côté de la décision dont l'utilisation est la meilleure.
La flotte est dimensionnée pour le pic et payée en continu
La capacité est provisionnée pour l'heure la plus chargée et louée pour toutes les autres. Une flotte dont le pic vaut trois fois sa moyenne tourne autour d'un tiers d'utilisation, si bien que chaque page utile porte environ trois fois le coût matériel que la fiche technique laisse entendre. L'autoscaling réduit cet écart sans le refermer : les navigateurs ont besoin de préchauffage, et s'adapter à une métrique qui bouge à la seconde signifie soit du retard, soit de la réserve.
Les pages en échec coûtent autant que les réussies
Une page qui renvoie un challenge, un timeout ou un blocage silencieux a consommé exactement le même contexte, la même mémoire et le même temps réel qu'une page qui a renvoyé des données. Sur une flotte auto-hébergée, vous payez à l'identique. Sur la Crawling API non : les requêtes en échec ne sont pas facturées, un réessai contre une cible instable change donc votre latence mais pas votre facture.
Cet écart grandit avec l'hostilité des cibles. Sur un corpus docile, c'est une erreur d'arrondi. Sur des sites dotés de vraies défenses anti-bot, où une part notable des tentatives finit en challenge, c'est une grande part de la facture, et un seul des deux modèles vous la fait payer.
Le chemin managé
Le chemin acheter supprime le control plane. Pas de pool, pas de préchauffage, pas de recyclage et rien à dimensionner, parce que le navigateur tourne de l'autre côté d'un appel API.
Source : final/src/crawlbase.ts
export async function crawlbaseRender( url: string, token: string, timeoutMs: number ): Promise<RenderResult> { const endpoint = `https://api.crawlbase.com/?token=${token}&url=${encodeURIComponent(url)}`; const response = await fetch(endpoint, { signal: controller.signal }); const body = await response.text(); const cbStatus = Number(response.headers.get('cb_status') ?? response.status); return { cbStatus, bytes: body.length, ms: Date.now() - started }; }
Le token JavaScript est ce qui en fait un navigateur plutôt qu'un fetch : il pilote un vrai moteur de rendu à l'autre bout. Lisez cb_status plutôt que le statut HTTP, car c'est le champ qui décrit ce qui est arrivé à la cible, par opposition à ce qui est arrivé à votre appel d'API.
Ici la concurrence est un réglage d'offre et non une flotte, et elle est relevée sur demande. C'est précisément pourquoi la comparer à un nombre de sessions est le mauvais geste : la comparaison utile se fait à débit égal. Prenez vos pages par jour, divisez par 86 400, multipliez par vos secondes par page, et comparez la concurrence qui en découle à la flotte que ce même nombre exigerait.
De la concurrence de rendu sans la flotte : un vrai navigateur s'exécute chez nous, derrière des IP résidentielles rotatives, et renvoie une réponse propre. Les requêtes en échec ne sont pas facturées, les cibles hostiles vous coûtent donc de la latence et non de la facture. Démarrez gratuitement avec 1 000 requêtes, sans carte.
Prendre la décision
Une fois la charge exprimée en pages par jour, le choix cesse d'être idéologique.
| Construire la flotte quand | Acheter le rendu quand |
|---|---|
| l'exécution du navigateur est elle-même le produit | le rendu alimente un produit qui est autre chose |
| vous avez besoin de contrôler tout le cycle de vie du navigateur | vous avez besoin de pages, pas de navigateurs |
| des ingénieurs plateforme sont déjà en poste et d'astreinte | ces postes seraient mieux employés en aval |
| l'utilisation est élevée et prévisible | la demande est en dents de scie, donc la capacité dimensionnée pour le pic tourne à vide |
| les cibles sont dociles et les taux d'échec faibles | les cibles se défendent et les échecs sont une vraie part des tentatives |
Les deux dernières lignes sont celles que les équipes sous-estiment. Ce sont aussi les deux que le modèle de capacité ne peut pas voir, car toutes deux sont des propriétés de la charge et non du matériel.
Conclusion
Dix mille sessions simultanées est une réponse à une question que la plupart des équipes n'ont pas posée précisément. Converti en charge, cela fait 172,8 millions de pages par jour ; converti en sens inverse, un million de pages par jour demande une concurrence de 58. Clarifier ces deux chiffres tranche plus du débat que n'importe quel benchmark.
Le control plane lui-même n'est pas la partie difficile, et l'implémentation de référence montre pourquoi : un plafond borné, un bail, un chemin de recyclage, et une incrémentation soigneusement placée avant un await. Cela fait quelques centaines de lignes, et c'est terminé.
Ce qui ne se termine jamais, c'est l'exploitation : 100 nœuds dimensionnés pour un pic sous lequel ils passent le plus clair de la journée, un processus navigateur par nœud qui livre cent sessions à un seul plantage, et une facture qui compte une page bloquée au même prix qu'une page livrée. La construction est un week-end. La flotte est une astreinte.
Foire aux questions
Une session est-elle un navigateur ou un contexte de navigateur ?
Un contexte de navigateur. Il porte des cookies, un stockage et un état d'exécution isolés pour une fraction du coût d'un processus Chromium séparé, et c'est ce qui rend cent sessions par nœud possibles. La contrepartie : tous les contextes d'un nœud partagent un processus navigateur, un plantage les emporte donc ensemble.
Pourquoi réserver le créneau de concurrence avant le await compte-t-il autant ?
Parce que await rend la main. Si le compteur est incrémenté après la création du contexte, chaque appelant qui arrive dans cet intervalle lit l'ancienne valeur et passe le contrôle du plafond, si bien qu'une limite configurée à cinq peut brièvement créer vingt contextes. Réserver le créneau de façon synchrone et l'annuler en cas d'échec, c'est la différence entre une limite et une suggestion.
Pourquoi distinguer sessions bornées par la RAM et sessions effectives par nœud ?
L'une est ce que la mémoire permet, l'autre ce que vous feriez réellement tourner. À 250 Mo par session sur un nœud de 32 Go dont 4 sont retenus, la mémoire permet 114 ; le plafond opérationnel de 100 est le nombre qui dimensionne la flotte. Tourner à la limite mémoire ne laisse rien pour un pic de trafic ou un contexte qui fuit.
Quelles hypothèses produisent l'estimation de 100 nœuds ?
250 Mo par session, 32 Go par nœud, 4 Go retenus par nœud, et un plafond opérationnel de 100 sessions par nœud, pour une cible de 10 000 sessions. Les quatre sont configurables dans capacity.ts, et les quatre méritent d'être remplacés par vos propres mesures avant de citer le résultat, car la mémoire par session en particulier varie énormément selon ce que les pages chargent vraiment.
Comment comparer une concurrence managée à un nombre de sessions ?
Vous ne le faites pas, ce sont des unités différentes. Convertissez d'abord les deux en débit : divisez vos pages par jour par 86 400 pour obtenir des pages par seconde, puis multipliez par vos secondes par page mesurées pour obtenir la concurrence dont chaque côté a besoin. Comparez à débit égal, et dimensionnez l'offre managée contre ce chiffre plutôt que contre le nombre de sessions affiché.
Les 37 requêtes par seconde de la démo prédisent-elles le débit de la flotte ?
Non. Ce chiffre vient de cinq contextes frappant example.com sur une seule machine : il mesure la logique d'allocation sur une page triviale, pas du rendu en conditions réelles. Une page lourde en JavaScript prend des secondes et non des millisecondes, et cette latence est l'entrée qui décide de la concurrence qu'un débit cible exige.
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.
