Irgendwann stellt jemand die Infrastrukturfrage: Was braucht es, um 10.000 gleichzeitige Browser-Sessions zu tragen? Das klingt nach einer Kapazitätsfrage mit einer Hardware-Antwort. In Wahrheit ist es eine Frage der Einheiten, und die Einheiten stimmen nicht.
Zehntausend gleichzeitige Sessions sind keine Last. Sie sind eine Bereitstellungsentscheidung. Die Last sind Seiten pro Tag, und beide verbindet eine einzige Zeile Arithmetik, die das gesamte Build-oder-Buy-Argument entscheidet, bevor irgendeine Hardware bepreist wird.
Dieser Beitrag baut das, was eine Browser-Flotte steuert, in TypeScript und Playwright: einen Session-Pool mit harter Concurrency-Grenze, Leasing und Recycling. Dann bepreist er die Flotte, die 10.000 Sessions tatsächlich erfordern, rechnet diese Zahl in die Last um, die sie bedient, und vergleicht sie mit Managed Rendering bei gleichem Durchsatz statt bei gleicher Schlagzeile.
- Concurrency ist Durchsatz multipliziert mit Service-Zeit. Bei fünf Sekunden pro gerenderter Seite sind 10.000 Sessions 2.000 Seiten pro Sekunde, also 172,8 Millionen pro Tag.
- Die meisten Teams, die nach 10.000 Sessions fragen, brauchen etwa 1% davon. Eine Million Seiten pro Tag braucht eine Concurrency von rund 58.
- Die Flotte, die 10.000 Sessions trägt, sind etwa 100 Knoten und 3,2 TB RAM, auf die Spitze ausgelegt und die übrige Zeit im Leerlauf.
- Eine blockierte Seite kostet eine selbst betriebene Flotte dieselben RAM-Sekunden wie eine erfolgreiche. Bei der Crawling API werden fehlgeschlagene Requests gar nicht abgerechnet.
- Jeder Context auf einem Knoten lebt in einem einzigen Browser-Prozess. Ein Absturz nimmt alle mit.
10.000 Sessions sind eine Bereitstellungszahl, keine Last
Little's Law ist die Umrechnung. Die Zahl der Operationen in Flug ist die Abschlussrate multipliziert mit der Dauer je Operation. Für eine Browser-Flotte heißt das: Concurrency ist Seiten pro Sekunde multipliziert mit Sekunden pro Seite.
Eine JavaScript-lastige Seite, gerendert bis domcontentloaded braucht ein paar Sekunden. Sagen wir fünf. Dann sind 10.000 gleichzeitige Sessions überhaupt keine Bedarfszahl, sondern das hier:
| Last | Seiten pro Sekunde | Benötigte Concurrency bei 5 s pro Seite |
|---|---|---|
| 1 Million Seiten/Tag | 11,6 | 58 |
| 10 Millionen Seiten/Tag | 115,7 | 579 |
| 172,8 Millionen Seiten/Tag | 2.000 | 10.000 |
Zehntausend gleichzeitige Sessions sind die Bereitstellung für 172,8 Millionen Seiten pro Tag. Wenn Ihr tatsächlicher Bedarf eine Million Seiten pro Tag ist, dann bedient ihn eine Concurrency von 58, und die Forderung nach 10.000 Sessions ist rund 173 Mal größer als die Last dahinter.
Das lohnt sich zu klären, bevor irgendetwas bepreist wird, denn der gesamte Build-oder-Buy-Vergleich ändert sich je nachdem, welche Zahl echt ist. Die ehrliche Version der Frage lautet also nicht "können wir 10.000 Sessions fahren", sondern "wie viele Seiten pro Tag, welche Latenz pro Seite, und welche Concurrency folgt aus diesem Paar?"
Die Referenzimplementierung
Das Begleitprojekt in ScraperHub/scaling-a-headless-browser-fleet-to-10000-concurrent-sessions ist in drei lauffähige Teile gegliedert, von denen jeder einen anderen Teil der Frage beantwortet.
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
Es braucht Node.js 18 oder neuer und Playwrights Chromium. Nur der Managed-Weg braucht ein 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
Eine Session ist ein Context, kein Browser
Die erste architektonische Entscheidung ist die Einheit der Concurrency. Hier ist eine Session ein Playwright Browser-Context, kein Browser-Prozess. Ein Context trägt eigene Cookies, eigenen Storage und eigenen Ausführungszustand und kostet einen Bruchteil eines frischen Chromium-Prozesses. Genau das macht hohe Dichte überhaupt möglich.
Quelle: 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(); } }
Die Page ist bewusst kurzlebig: öffnen, navigieren, ein Signal lesen, schließen. Kurze Page-Lebensdauern verhindern, dass sich Zustand ansammelt, sodass ein einzelner Context viele Aufgaben nacheinander bedient, während der Browser-Lebenszyklus in der Factory bleibt.
Die Factory startet ein Chromium und legt jeden Context darin an. Das macht einen Context billig, und es bedeutet zugleich, dass ein Browser-Absturz jede Session auf diesem Knoten mitnimmt. Bei einer betrieblichen Obergrenze von 100 Sessions pro Knoten sind das 100 verlorene Sessions pro Absturz, und der healthy() Check des Pools bemerkt es pro Session, im Nachhinein, beim nächsten Acquire oder Release. Dichte und Wirkungsradius sind dasselbe Stellrad.
Die Control Plane ist der Ort der Garantie
Der Pool entscheidet, ob ein Aufrufer eine bestehende Session bekommt, eine neue oder eine Wartezeit. Diese drei Ausgänge sind die gesamte Stabilität der Flotte.
Quelle: 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); });
Ein Detail trägt die ganze Garantie: Der Slot wird reserviert, bevor auf factory.create()gewartet wird. Weil await die Ausführung abgibt, würden sonst mehrere Aufrufer alle live < maxConcurrency lesen, während der erste Context noch erzeugt wird. Eine Grenze von fünf könnte kurz zwanzig werden. Synchron zu reservieren und im Fehlerfall zurückzurollen macht aus der Grenze eine echte statt einer empfohlenen.
Der Release-Pfad schließt den Kreis. Eine Session, die zu alt ist oder ihren Health-Check nicht besteht, wird recycelt, und wenn bereits Aufrufer warten, erzeugt der Pool sofort Ersatz, damit die Kapazität nicht still schrumpft, sobald eine Session in Rente geht.
Wie es unter Contention aussieht
Starten Sie mehr Aufgaben, als die Grenze erlaubt:
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%
Die zweite Zeile ist die entscheidende. Zwanzig Aufgaben liefen durch, fünf Contexts existierten, und peakInUse überschritt die Grenze nie. Die übrigen fünfzehn Aufgaben wurden bedient, indem dieselben fünf Sessions verliehen und zurückgegeben wurden. Setzen Sie MAX_SESSION_AGE_MS=0 und stattdessen steigt der Zähler recycled , was den Rentenpfad übt, der lang laufende Flotten davor bewahrt, alten Zustand anzusammeln.
Beachten Sie, was dieser Lauf nicht zeigt. Es sind fünf Contexts gegen example.com auf einer Maschine, das validiert die Allokationslogik und nichts über Skalierung. Die Durchsatzzahl von 37 Requests pro Sekunde ist eine Eigenschaft einer trivialen Seite und eines lokalen Browsers, keine Prognose.
Die Flotte bepreisen
Das Kapazitätsmodell wandelt Annahmen pro Session in eine Knotenzahl und eine RAM-Rechnung. Es ist bewusst klein, denn es geht um die Arithmetik, nicht um das Werkzeug.
Quelle: 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);
Mit 250 MB pro Session, 32-GB-Knoten, die je 4 GB zurückhalten, und einer betrieblichen Obergrenze von 100 Sessions pro Knoten:
target sessions: 10000 RAM-bound sessions/node: 114 effective sessions/node: 100 nodes required: 100 total fleet RAM: ~3200 GB
Der Speicher allein würde 114 Sessions pro Knoten erlauben. Das Modell nimmt 100, weil das die Zahl ist, die Sie wirklich fahren würden, und die Lücke dazwischen ist der Unterschied zwischen Datenblatt und Produktionsflotte. Das Ergebnis sind rund 100 Knoten und 3,2 TB RAM, noch vor Autoscaling, Pflege der Browser-Images, Absturzbehandlung, Monitoring, Deployments und der Rufbereitschaft, die das Ganze am Leben hält.
Zwei Kosten, die die Knotenzahl nicht zeigt
Die 100-Knoten-Zahl ist der Listenpreis. Zwei Dinge bewegen den echten, und beide begünstigen die Seite der Entscheidung mit der besseren Auslastung.
Die Flotte ist auf die Spitze ausgelegt und wird durchgehend bezahlt
Kapazität wird gegen die geschäftigste Stunde bereitgestellt und für alle anderen mitgemietet. Eine Flotte, deren Spitze dreimal ihrem Mittel entspricht, läuft bei etwa einem Drittel Auslastung, sodass jede nützliche Seite grob das Dreifache der Hardwarekosten trägt, die das Datenblatt nahelegt. Autoscaling verkleinert diese Lücke und schließt sie nicht: Browser brauchen Warmlaufzeit, und auf eine Metrik zu skalieren, die sich im Sekundentakt bewegt, bedeutet entweder Verzug oder Reserve.
Fehlgeschlagene Seiten kosten so viel wie erfolgreiche
Eine Seite, die eine Challenge, einen Timeout oder einen weichen Block zurückgibt, hat exakt denselben Context, denselben Speicher und dieselbe Wanduhrzeit verbraucht wie eine Seite mit Daten. Auf einer selbst betriebenen Flotte zahlen Sie identisch dafür. Bei der Crawling API nicht: Fehlgeschlagene Requests werden nicht abgerechnet, ein Retry gegen ein wackliges Ziel ändert also Ihre Latenz, aber nicht Ihre Rechnung.
Dieser Unterschied skaliert damit, wie feindlich die Ziele sind. Auf einem gutmütigen Korpus ist er ein Rundungsfehler. Auf Seiten mit echter Anti-Bot-Abwehr, wo ein spürbarer Teil der Versuche in einer Challenge endet, ist er ein großer Teil der Rechnung, den nur eines der beiden Modelle Ihnen berechnet.
Der Managed-Weg
Der Buy-Weg löscht die Control Plane. Es gibt keinen Pool, kein Warmlaufen, kein Recycling und nichts zu planen, weil der Browser auf der anderen Seite eines API-Aufrufs läuft.
Quelle: 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 }; }
Das JavaScript-Token macht daraus einen Browser statt eines Fetch: Es treibt eine echte Rendering-Engine auf der Gegenseite an. Lesen Sie cb_status statt des HTTP-Status, denn das ist das Feld, das beschreibt, was mit dem Zielpassiert ist, im Unterschied zu dem, was mit Ihrem API-Aufruf passiert ist.
Concurrency ist hier eine Tarifeinstellung und keine Flotte, und sie wird auf Anfrage erhöht. Genau deshalb ist der Vergleich mit einer Session-Zahl der falsche Zug: Der nützliche Vergleich läuft bei gleichem Durchsatz. Nehmen Sie Ihre Seiten pro Tag, teilen Sie durch 86.400, multiplizieren Sie mit Ihren Sekunden pro Seite, und vergleichen Sie die daraus folgende Concurrency mit der Flotte, die dieselbe Zahl verlangen würde.
Rendering-Concurrency ohne Flotte: Ein echter Browser läuft auf unserer Seite, hinter rotierenden Residential-IPs, und liefert eine saubere Antwort zurück. Fehlgeschlagene Requests werden nicht abgerechnet, feindliche Ziele kosten Sie also Latenz statt Rechnung. Starten Sie kostenlos mit 1.000 Requests, ohne Karte.
Die Entscheidung treffen
Sobald die Last in Seiten pro Tag ausgedrückt ist, hört die Wahl auf, ideologisch zu sein.
| Flotte selbst bauen, wenn | Rendering einkaufen, wenn |
|---|---|
| Browser-Ausführung selbst das Produkt ist | Rendering ein Produkt speist, das etwas anderes ist |
| Sie Kontrolle über den ganzen Browser-Lebenszyklus brauchen | Sie Seiten brauchen, keine Browser |
| Platform-Engineers ohnehin angestellt und in Rufbereitschaft sind | diese Stellen weiter hinten besser eingesetzt sind |
| die Auslastung hoch und vorhersehbar ist | die Nachfrage stoßweise kommt und auf die Spitze ausgelegte Kapazität leerläuft |
| die Ziele friedlich sind und Fehlerraten niedrig | die Ziele sich wehren und Fehler ein echter Teil der Versuche sind |
Die letzten beiden Zeilen unterschätzen Teams am ehesten. Es sind auch die beiden, die das Kapazitätsmodell nicht sehen kann, denn beide sind Eigenschaften der Last und nicht der Hardware.
Fazit
Zehntausend gleichzeitige Sessions sind die Antwort auf eine Frage, die die meisten Teams nicht präzise gestellt haben. In eine Last umgerechnet sind es 172,8 Millionen Seiten pro Tag; zurückgerechnet braucht eine Million Seiten pro Tag eine Concurrency von 58. Diese beiden Zahlen zu klären entscheidet mehr vom Streit als jeder Benchmark.
Die Control Plane selbst ist nicht der schwere Teil, und die Referenzimplementierung zeigt warum: eine feste Grenze, ein Lease, ein Recycling-Pfad und ein sorgfältig platziertes Inkrement vor einem await. Das sind ein paar hundert Zeilen, und sie sind fertig.
Was nicht fertig wird, ist der Betrieb: 100 Knoten, ausgelegt auf eine Spitze, unter der sie den größten Teil des Tages liegen, ein Browser-Prozess pro Knoten, der hundert Sessions einem einzigen Absturz ausliefert, und eine Rechnung, die für eine blockierte Seite dasselbe verlangt wie für eine gelieferte. Der Bau ist ein Wochenende. Die Flotte ist eine Rufbereitschaft.
Häufig gestellte Fragen
Ist eine Session ein Browser oder ein Browser-Context?
Ein Browser-Context. Er trägt isolierte Cookies, isolierten Storage und isolierten Ausführungszustand zu einem Bruchteil der Kosten eines eigenen Chromium-Prozesses, und genau das macht hundert Sessions pro Knoten überhaupt machbar. Der Preis dafür: Jeder Context auf einem Knoten teilt sich einen Browser-Prozess, ein Absturz nimmt also alle gemeinsam mit.
Warum ist es so wichtig, den Concurrency-Slot vor dem await zu reservieren?
Weil await die Ausführung abgibt. Wird der Zähler erst nach dem Erzeugen des Contexts erhöht, liest jeder Aufrufer, der in dieser Lücke ankommt, den alten Wert und besteht die Prüfung, sodass eine konfigurierte Grenze von fünf kurzzeitig zwanzig Contexts erzeugen kann. Den Slot synchron zu reservieren und im Fehlerfall zurückzurollen ist der Unterschied zwischen einer Grenze und einem Vorschlag.
Warum zwischen RAM-gebundenen und effektiven Sessions pro Knoten unterscheiden?
Das eine ist, was der Speicher erlaubt, das andere, was Sie wirklich fahren würden. Bei 250 MB pro Session auf einem 32-GB-Knoten mit 4 GB Rückhalt erlaubt der Speicher 114; die betriebliche Obergrenze von 100 ist die Zahl, die die Flotte dimensioniert. Am Speicherlimit zu fahren lässt nichts übrig für eine Verkehrsspitze oder einen leckenden Context.
Welche Annahmen ergeben die Schätzung von 100 Knoten?
250 MB pro Session, 32 GB pro Knoten, 4 GB Rückhalt pro Knoten und eine betriebliche Obergrenze von 100 Sessions pro Knoten, gegen ein Ziel von 10.000 Sessions. Alle vier sind konfigurierbar in capacity.ts, und alle vier sollten Sie durch eigene Messungen ersetzen, bevor Sie das Ergebnis zitieren, denn gerade der Speicher pro Session schwankt enorm damit, was die Seiten tatsächlich laden.
Wie vergleiche ich Managed Concurrency mit einer Session-Zahl?
Gar nicht, denn es sind verschiedene Einheiten. Rechnen Sie zuerst beides in Durchsatz um: Teilen Sie Ihre Seiten pro Tag durch 86.400, um Seiten pro Sekunde zu erhalten, und multiplizieren Sie dann mit Ihren gemessenen Sekunden pro Seite, um die Concurrency zu bekommen, die jede Seite braucht. Vergleichen Sie bei gleichem Durchsatz und dimensionieren Sie den Tarif gegen diese Zahl statt gegen die Schlagzeilen-Session-Zahl.
Sagen die 37 Requests pro Sekunde aus der Demo den Flottendurchsatz voraus?
Nein. Diese Zahl kam von fünf Contexts gegen example.com auf einer Maschine, sie misst also die Allokationslogik an einer trivialen Seite und kein Rendering unter echten Bedingungen. Eine JavaScript-lastige Seite braucht Sekunden statt Millisekunden, und diese Latenz ist die Eingabe, die entscheidet, wie viel Concurrency ein Zieldurchsatz verlangt.
Crawlen Sie jede Website im großen Maßstab, ohne gegen die Infrastruktur zu kämpfen.
Crawlbase übernimmt Proxys, Fingerprints und CAPTCHAs, damit Ihr Team Datenpipelines ausliefert, statt Crawl-Infrastruktur zu pflegen. 1.000 Anfragen kostenlos, keine Karte erforderlich.
