Gehostete Browser
Starten Sie mit einem einzigen API-Aufruf einen Clearcote-Browser auf unseren Servern und steuern Sie ihn über das Chrome DevTools Protocol, aus Playwright, Puppeteer oder jedem anderen CDP-Client. Sie müssen nichts selbst installieren oder betreiben. Der Traffic läuft standardmäßig über Residential-IPs, und Sie zahlen pro GB aus einem Prepaid-Guthaben.
- Residential-IPs statt Rechenzentrum. Websites sehen einen echten privaten Internetanschluss bei einem Endkunden-Provider, keine Hosting- oder Cloud-Adresse.
- Echte Hardware statt VPS. Die Browser laufen auf unseren eigenen dedizierten physischen Servern, nicht auf geteilten virtuellen Maschinen in der Cloud.
Gratis€5 Traffic gratis, wenn Sie GitHub verbinden. Ohne Kreditkarte.
Einmaliges Guthaben, eines pro GitHub-Konto. Das GitHub-Konto muss mindestens 30 Tage alt sein.
Preise
- €1.00 pro GB, Residential-Proxy inklusive. Standardmäßig geht jede Session über eine Residential-IP ins Internet: einen echten privaten Anschluss bei einem Endkunden-Provider, keine Rechenzentrums- oder Hosting-Adresse. Genau diesen Traffic bezahlen Sie mit den €1.00; eine separate Proxy-Rechnung gibt es nicht.
- Gemessen wird der Traffic zwischen Browser und Internet, Upload und Download zusammengerechnet (1 GB = 109 Byte). Siehe Was als Traffic zählt.
- Keine Kosten für Laufzeit, Sessions oder CDP-Nachrichten.
- Prepaid: Guthaben laden Sie im Dashboard auf. Ein Browser braucht zum Start mindestens €0.50, und jede Session ist auf den Betrag begrenzt, den das Guthaben beim Start abdecken kann – geteilt zwischen allen Browsern, die gerade laufen (ist Ihr eigenes
maxGbniedriger, gilt dieses). Ein laufender Browser wird gestoppt, sobald das Guthaben null erreicht (die Nutzung wird etwa alle 15 Sekunden gemeldet, daher kann die letzte Meldung es knapp unter null drücken).
Was als Traffic zählt
Gezählt wird jedes Byte, das der Browser an eine Website sendet oder von ihr empfängt, und zwar direkt auf der Leitung – genau so, wie ein Proxy-Anbieter zählt. Für eine typische Seite heißt das:
- Gezählt: die Seite selbst und alles, was sie nachlädt: Skripte, Stylesheets, Bilder, Fonts, Videos, API-Aufrufe, Werbung und Tracker, WebSockets, dazu Request-Header, Cookies und der Verschlüsselungs-Overhead (TLS) jeder Verbindung. Seiten laden weiter, während Sie warten, daher zählen auch Polling im Hintergrund und Analytics mit.
- Nicht gezählt: die CDP-Verbindung zwischen Ihrem Code und dem Browser (Befehle, Ergebnisse, Screenshots, PDFs, ausgelesene Seiteninhalte), die Live-Ansicht im Dashboard und alles, was der Browser gar nicht erst abruft (Requests, die Sie blockieren, Dateien aus seinem eigenen Cache).
Als grobe Orientierung: Eine schlanke Textseite kostet deutlich unter 1 MB, eine typische News- oder Shop-Seite 2 bis 5 MB und eine schwere Single-Page-App oder alles mit Video 10 MB oder mehr. Bei €1.00 pro GB ergeben 1.000 Seiten à 3 MB rund 3 GB. Die echten Zahlen zeigt Ihre eigene Session: Das Dashboard listet für jede Session den Traffic und die 20 Sites mit den meisten Bytes, und maxGb deckelt eine Session, damit eine außer Kontrolle geratene Seite nicht Ihr Guthaben auffrisst.
Traffic reduzieren
Den Großteil des Gewichts einer Seite macht meist aus, was ein Skript gar nicht braucht. Die größten Einsparungen, der Reihe nach:
- Bilder, Medien und Fonts blockieren. Oft die Hälfte einer Seite oder mehr. Blockieren Sie sie per URL-Muster im Browser, dann verlassen die Anfragen den Browser gar nicht erst und werden nie berechnet – und der Browser behält seinen Cache:
// Playwright: block by URL pattern over CDP (keeps the browser cache on)
const cdp = await context.newCDPSession(page);
await cdp.send("Network.enable");
await cdp.send("Network.setBlockedURLs", {
urls: ["*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp", "*.svg", "*.woff", "*.woff2", "*.ttf", "*.mp4", "*.webm"],
});
// Puppeteer: the same, through its CDP session
const client = await page.createCDPSession();
await client.send("Network.enable");
await client.send("Network.setBlockedURLs", { urls: ["*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp", "*.woff2"] });Verwenden Sie page.route() / context.route() und die Request-Interception von Puppeteer nicht nur, um Requests zu verwerfen: Sie schalten den Browser-Cache ab, sodass jede Seite ihre Skripte und Styles erneut herunterlädt – und das kostet mehr, als die eingesparten Bilder bringen. Setzen Sie sie nur ein, wenn Sie Requests verändern müssen. Die Option adblock blockiert Werbung und Tracker außerdem für Sie.
- Werbung, Analytics und Tracker blockieren. Brechen Sie Requests an Drittanbieter-Domains ab, die Sie nicht brauchen. Die Top-Sites-Liste der Session im Dashboard zeigt, welche Sie am meisten kosten.
- Nicht länger warten als nötig.
waitUntil: "networkidle"wartet auf alles, was eine Seite lädt, auch auf Werbung. Nehmen Sie lieber"domcontentloaded"und warten Sie danach auf das eine Element, das Sie wirklich brauchen. - Den Browser schließen, sobald Sie fertig sind. Eine offene Seite pollt im Hintergrund weiter. Senken Sie
idleTimeoutSec, damit sich eine vergessene Session von selbst schließt. - Einen Browser für viele Seiten nutzen. Dank seines Caches werden Skripte und Styles, die sich Seiten derselben Site teilen, nur einmal geladen statt für jede Seite. Navigieren Sie in derselben Session, statt für jede URL eine neue zu starten.
- Wenn möglich, die API der Site aufrufen. Sobald der Browser eine funktionierende Session hat, ist ein
fetch()aus der Seite heraus nach dem benötigten JSON nur ein Bruchteil dessen, was ein erneutes Laden der Seite kostet. - Deckeln. Setzen Sie
maxGbfür jede Session, damit eine unerwartet schwere Seite stoppt, statt Ihr Guthaben leerzuziehen.
Blocking funktioniert auf den meisten Sites, einige prüfen aber, ob Bilder oder Fonts tatsächlich geladen wurden. Verhält sich eine Site mit aktivem Blocking anders, lassen Sie diesen Typ für diese Site wieder zu.
Möchten Sie es erst in Aktion sehen? Der Playground führt ein Skript in einem Cloud-Browser direkt aus Ihrem Dashboard aus, mit Live-Ansicht, Konsolenausgabe und Screenshots nebeneinander.
1. API-Key erstellen
Legen Sie einen auf der Seite API-Keys an. Er beginnt mit cc_live_ und wird als Bearer-Token gesendet. Halten Sie ihn geheim: Wer ihn besitzt, kann Ihr Guthaben ausgeben.
2. Browser starten und verbinden
POST /api/v1/browsers liefert eine connectUrl: eine WebSocket-URL für genau diesen Browser, die nur einmal gilt. Verbinden Sie sich innerhalb von zwei Minuten; ein zweites Mal lässt sie sich nicht verwenden.
// Node.js + Playwright
import { chromium } from "playwright";
const res = await fetch("https://www.clearcotelabs.com/api/v1/browsers", {
method: "POST",
headers: { authorization: "Bearer cc_live_...", "content-type": "application/json" },
body: JSON.stringify({ identity: "account-1", country: "us" }),
});
const { connectUrl, id, error } = await res.json();
if (error) throw new Error(error);
const browser = await chromium.connectOverCDP(connectUrl);
const context = browser.contexts()[0];
const page = context.pages()[0] ?? (await context.newPage());
await page.goto("https://example.com");
await browser.close(); // ends the session// Puppeteer: the same connectUrl
const browser = await puppeteer.connect({ browserWSEndpoint: connectUrl, defaultViewport: null });# Python + Playwright
import requests
from playwright.sync_api import sync_playwright
r = requests.post("https://www.clearcotelabs.com/api/v1/browsers",
headers={"authorization": "Bearer cc_live_..."},
json={"identity": "account-1", "country": "de"}).json()
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(r["connectUrl"])
page = browser.contexts[0].new_page()
page.goto("https://example.com")
browser.close()Der Create-Aufruf antwortet mit 201 und allem, was Ihr Code braucht, um sich zu verbinden und den Preis zu kennen:
{
"id": "bs_…", // use it with GET / DELETE /api/v1/browsers/<id>
"connectUrl": "wss://…/v1/connect/bs_…?token=…",
"expiresAt": "2026-09-24T10:02:00.000Z", // connect before this (two minutes)
"worker": "w_…",
"pricing": { "eurPerGb": 1, "eurPerHour": 0 },
"limits": { "maxSeconds": 14400, "idleSeconds": 300 } // plus maxBytes when capped
}Optionen
Alle optional. Senden Sie sie als JSON-Body des Create-Aufrufs.
| Feld | Typ | Bedeutung |
|---|---|---|
identity | string | One label per account you run: the same device fingerprint AND the same residential IP, for as long as that IP stays online. |
fingerprint | string | Device seed only (no IP pinning). The same seed gives the same device profile every time; with lightStealth on it picks from a small set of metadata profiles. |
lightStealth | boolean | Default true: varies only the metadata axes the host can back up. Set false to turn it off. |
platform | windows | macos | linux | android | Operating system the persona presents. |
brand | Chrome | Edge | Opera | Vivaldi | Browser brand the persona presents. |
timezone | IANA name | e.g. America/New_York. Use geoip instead to follow the exit IP. |
locale | string | Accept-Language, e.g. en-US,en. |
geoip | boolean | Timezone and language follow the exit IP. Default true unless you set timezone or locale yourself. |
proxy | "managed" | { server, username?, password? } | Omitted = managed residential pool. Or your own proxy: http://, socks5:// or socks5h:// (rules below). |
country | 2-letter code | Managed pool: exit country, e.g. us, de, gb. |
state | region code | Managed pool: exit state/region, e.g. ca, ny. Needs country. |
city | city name | Managed pool: exit city, e.g. "los angeles". Needs state. |
proxySession | string | Managed pool: sticky label. The same label returns the same exit IP later (kept for 24 hours). |
timeoutSec | number | Hard limit on the session length, in seconds (10 up to the account maximum below). |
idleTimeoutSec | number | End the session after this long without a CDP command (10–1800). |
maxGb | number | Stop the session after this much traffic (0.001–1000). |
headless | boolean | Default true. |
keepAlive | boolean | Default false. Keep the browser running when your client disconnects, until you end it (DELETE, or the CDP command Browser.close) or a limit does; reconnect with POST /api/v1/browsers/<id>/connect. |
version | string | Run a specific Clearcote release, e.g. "152.0.7977.82-r21" or "r21". Omitted = the current release. See "Pinning a release". |
profile | "name" | { name, persist? } | Load a saved profile (cookies + site storage). With persist: true, save it back when the session ends. See "Profiles". |
url | http(s) URL | Opened in the first tab before you connect: you find it already loading. |
adblock | boolean | Refuse known ad and tracker hosts before they load, so they are never billed. Default false. |
note | string | Your label for the session (at most 256 characters). Shown in the dashboard; filter by it in the list. |
worker | string | Place the session on the same server as an earlier one (its worker). 503 if that server is full. |
Stealth-Defaults und Identitäten
Jede Session startet mit den Einstellungen von der Seite Empfohlene Einstellungen: lightStealth an, ein Seed sowie Zeitzone und Sprache passend zur Exit-IP. Mit lightStealth (dem Standard) wählt der Seed das Geräteprofil aus einer kleinen Auswahl, die CPU-Kerne, Arbeitsspeicher und Pixel-Ratio variiert; Canvas, WebGL und Audio sind die der Maschine, auf der die Session läuft. Setzen Sie lightStealth: false für eine vollständige Persona pro Seed: Canvas- und WebGL-Readbacks bekommen aus dem Seed ein Rauschen pro Site, und GPU-Strings, Bildschirm und Audio-Einstellungen (Sample-Rate, Latenz) folgen der Persona. Ohne identity oder fingerprint erhält jede Session einen zufälligen Seed und eine neue IP.
Mit identity: "account-42" kommen Sie in späteren Sessions mit demselben Geräteprofil zurück, und zwar auf derselben IP, solange diese Residential-IP online bleibt – genau das erwartet ein eingeloggter Account. Identitäten gelten nur innerhalb Ihres Accounts: Ein anderer Kunde mit demselben Label erhält seinen eigenen Seed und seine eigene IP. Eine Identität allein speichert keine Cookies; dafür verwenden Sie ein Profil.
Profile: einmal anmelden
Ein Profil speichert Cookies, localStorage und IndexedDB einer Session unter einem Namen, sodass die nächste Session mit diesem Namen bereits angemeldet startet. Übergeben Sie profile: { name: "shop-account", persist: true }, um es zu laden und beim Ende der Session zurückzuspeichern, oder nur profile: "shop-account", um es schreibgeschützt zu laden. Die erste Session mit einem neuen Namen startet leer und legt das Profil an.
// Every run: the same body. The first one starts signed out; sign in, then close the browser
// and the session saves the cookies and site storage. Every later run starts signed in.
const res = await fetch("https://www.clearcotelabs.com/api/v1/browsers", {
method: "POST",
headers: { authorization: "Bearer cc_live_...", "content-type": "application/json" },
body: JSON.stringify({ profile: { name: "shop-account", persist: true }, country: "de" }),
});- Gleiches Gerät, gleiche IP. Ein Profil bringt seine eigene Identität mit (
profile:<name>), sodass die Site denselben Fingerprint sieht und, solange diese IP online bleibt, dieselbe Residential-IP – wie bei einem wiederkehrenden Kunden. Übergeben Sie selbstidentityoderfingerprint, wenn Sie etwas anderes wollen, und behalten Sie zwischen den Läufen dasselbe Land bei. - Immer nur ein Schreiber. Nur eine laufende Session darf in ein Profil speichern; eine zweite mit
persist: trueerhält409 PROFILE_IN_USE. Schreibgeschützte Sessions können parallel laufen und sehen den zuletzt gespeicherten Stand. - Gespeichert wird beim Ende der Session, egal ob Sie den Browser schließen, die Verbindung trennen, ihn stoppen oder ein Limit die Session beendet. Ist der Browser abgestürzt, bleibt der zuvor gespeicherte Stand erhalten, statt durch einen unvollständigen ersetzt zu werden. Session-Cookies (ohne Ablaufdatum) werden verworfen, so wie ein echter Browser sie beim Neustart verwirft.
- Bis etwa 3,5 MB komprimiert. Wird es durch die IndexedDB einer Site größer, bleibt die IndexedDB außen vor; Cookies und localStorage werden trotzdem gespeichert.
- Privat. Ein Profil gehört nur Ihnen („shop-account“ eines anderen Kunden ist ein anderes Profil), wird verschlüsselt gespeichert und nur an den Server übergeben, auf dem Ihre Session läuft. Auflisten mit
GET /api/v1/browsers/profiles, löschen mitDELETE /api/v1/browsers/profiles/<name>, oder Sie nutzen das Dashboard.
Exit-IPs: Rotation, Sticky Sessions und Geo-Targeting
- Standard: Jede Browser-Session erhält ihre eigene Residential-Exit-IP und behält sie für die gesamte Session.
- Sticky: Übergeben Sie dasselbe
proxySession-Label (zum Beispiel eines pro verwaltetem Account), um in einer späteren Session dieselbe Exit-IP zurückzubekommen. Labels gelten nur innerhalb Ihres Accounts. Eine Residential-IP bleibt verfügbar, solange ihr Peer online ist, typischerweise mehrere Stunden; geht er offline, erhalten Sie eine andere IP aus demselben Netz. - Standort:
country, dann optionalstateundcity. Je enger das Ziel, desto kleiner der Pool. - Eigener Proxy:
proxy: { server: "http://host:port", username, password }odersocks5://host:port(Namensauflösung bei uns) bzw.socks5h://host:port(Namensauflösung durch Ihren Proxy). Die Anfrage wird streng geprüft: ein expliziter Port, Zugangsdaten inusername/password(eine URL der Formuser:pass@hostergibt einen 400; jeweils höchstens 255 Byte), undcountry,state,cityundproxySessionwerden abgelehnt statt ignoriert, weil sie den verwalteten Pool beschreiben. Die Adresse des Proxys selbst muss öffentlich sein. Der Traffic wird genauso abgerechnet.
Zeitzone und Sprache folgen standardmäßig der Exit-IP (geoip). Mit timezone / locale legen Sie sie selbst fest, mit geoip: false schalten Sie das ab.
Release pinnen
Sessions laufen mit dem aktuellen Clearcote-Release. Für ein älteres übergeben Sie version: das vollständige Release ("152.0.7977.82-r21"), nur den Rebuild ("r21"), eine Chromium-Version oder Major-Version ("152" wählt deren neuesten Build) oder "latest". Es sind dieselben Releases, die die SDK-Option version herunterlädt – eine gehostete Session und ein lokaler Lauf mit demselben Pin verwenden also denselben Build.
const res = await fetch("https://www.clearcotelabs.com/api/v1/browsers", {
method: "POST",
headers: { authorization: "Bearer cc_live_...", "content-type": "application/json" },
body: JSON.stringify({ identity: "account-1", version: "152.0.7977.82-r21" }),
});
const { connectUrl, engine, warnings } = await res.json();
// engine -> { version: "152.0.7977.82", revision: "r21", pinned: true }
// warnings -> [ "This session runs 152.0.7977.82-r21, older than the current ... " ]- Prüfen Sie
warnings. Einem älteren Release fehlt, was spätere Releases hinzugefügt haben. Optionen und Fixes, die danach kamen, können fehlen oder stillschweigend ignoriert statt abgelehnt werden. Eine Einstellung, die auf dem aktuellen Release funktioniert, kann auf einem gepinnten also unbemerkt wirkungslos bleiben. enginein der Antwort nennt immer das Release, mit dem die Session läuft, ob gepinnt oder nicht.- Ein Release, das es nicht gibt, ergibt einen
400mit dem CodeUNKNOWN_VERSION, und die Meldung listet die Releases auf, die Sie wählen können. - Die erste Session auf einem Release, das unsere Server noch nicht verwendet haben, kann bis zu einer Minute länger zum Starten brauchen, während der Build geladen wird. Weitere Sessions darauf starten so schnell wie jede andere.
Startseite und Werbeblocker
urlöffnet eine Seite im ersten Tab, bevor Sie sich verbinden – sie lädt also schon, wenn sich Ihr Skript anhängt.adblock: truelehnt Requests an bekannte Werbe-, Ad-Verification- und Analytics-Hosts ab, bevor sie überhaupt gestellt werden, sodass sie nie berechnet werden. Die Liste ist bewusst konservativ (Tag-Manager, Consent-Tools, Login-SDKs und CAPTCHAs bleiben unangetastet), aber einige Sites bemerken fehlende Werbung; lassen Sie die Option dort aus, wo das eine Rolle spielt.
Live-Ansicht: zusehen, übernehmen, teilen
Klicken Sie im Dashboard auf eine Session, um zu sehen, an welche Sites ihr Traffic ging, und um sie live zu verfolgen. Über Take control können Sie selbst darin klicken, tippen, scrollen, einfügen und navigieren, etwa um sich anzumelden oder eine Prüfung zu bestehen, an der Ihr Skript scheitert. Ihr Skript bleibt die ganze Zeit verbunden; pausieren Sie es also, während Sie eingreifen. Menschliche Eingaben zählen als Aktivität, daher wird eine Session, die Sie gerade steuern, nicht wegen Inaktivität geschlossen. Share erzeugt einen Link, den jeder ohne Account öffnen kann, nur zum Zusehen oder mit Steuerung, für 15 Minuten bis 4 Stunden und nie über das Ende der Session hinaus.
Über die API:
# a live-view WebSocket for a running session (open it within 60 s)
# binary messages are JPEG frames, text messages are {"url","title","tabs"}
curl -H "authorization: Bearer cc_live_..." https://www.clearcotelabs.com/api/v1/browsers/<id>/live
# with control: the answer says "interactive": true when it was granted
curl -H "authorization: Bearer cc_live_..." "https://www.clearcotelabs.com/api/v1/browsers/<id>/live?control=1"
# a share link: control optional, 1 to 240 minutes (default 30)
curl -X POST -H "authorization: Bearer cc_live_..." -H "content-type: application/json" -d '{"control": false, "minutes": 60}' https://www.clearcotelabs.com/api/v1/browsers/<id>/shareMit Steuerung senden Sie JSON-Textnachrichten über denselben WebSocket. Koordinaten sind Anteile (0 bis 1) des Frames, den Sie gerade sehen; alles andere wird ignoriert.
| Nachricht | Wirkung |
|---|---|
{"t":"mouse", | Drücken (down), Loslassen (up) oder move; n ist die Anzahl der Klicks, m sind die Modifier-Tasten (Alt 1, Ctrl 2, Meta 4, Shift 8). |
{"t":"wheel", | An einer Position um eine Anzahl Pixel scrollen. |
{"t":"key", | Eine Taste wird gedrückt oder losgelassen, so wie eine Tastatur es sendet. |
{"t":"text", | Text einfügen, als wäre er getippt (bis zu 5000 Zeichen). |
{"t":"nav",, forward, reload oder {"t":"nav", | Verlauf, Neuladen oder eine http(s)-Adresse öffnen. |
GET /api/v1/browsers/<id> enthält traffic: die 20 Sites mit den meisten Bytes in dieser Session.
Sessions verwalten
# one session: status, traffic, seconds, cost so far
curl -H "authorization: Bearer cc_live_..." https://www.clearcotelabs.com/api/v1/browsers/<id>
# stop it (a running browser closes within about 15 seconds)
curl -X DELETE -H "authorization: Bearer cc_live_..." https://www.clearcotelabs.com/api/v1/browsers/<id>
# balance + your 20 most recent sessions
curl -H "authorization: Bearer cc_live_..." https://www.clearcotelabs.com/api/v1/browsers
# filtered by status and note text, up to 100; page back with before=<a createdAt you got>
curl -H "authorization: Bearer cc_live_..." "https://www.clearcotelabs.com/api/v1/browsers?status=active,ended¬e=shop-de&limit=50"
# label a session (null clears it)
curl -X PATCH -H "authorization: Bearer cc_live_..." -H "content-type: application/json" -d '{"note": "shop-de nightly"}' https://www.clearcotelabs.com/api/v1/browsers/<id>Geben Sie einer Session beim Anlegen eine note mit, um sie in der Liste und im Dashboard wiederzufinden. Um eine Session auf demselben Server wie eine frühere zu starten (warme Caches, dieselbe Maschine), übergeben Sie den worker jener Session; ist dieser Server voll, erhalten Sie einen 503 und keinen anderen Server.
Eine Session endet außerdem, wenn Sie den Browser schließen oder die Verbindung trennen, und wenn eines der unten genannten Limits erreicht ist. Eine Stop-Anfrage beendet eine Session, mit der sich niemand verbunden hat, sofort; einen laufenden Browser schließt sein Server innerhalb eines Meldeintervalls, also nach etwa 15 Sekunden. GET antwortet mit:
{
"id": "bs_…",
"status": "active", // see the table below
"proxy": "managed", // or "custom"
"createdAt": "…", "startedAt": "…", "endedAt": null,
"endReason": null, // set once ended, e.g. "user", "balance", "launch_failed"
"stopRequested": false,
"usage": { "bytesUp": 120334, "bytesDown": 4812009, "gb": 0.0049, "seconds": 41 },
"traffic": [ { "site": "example.com", "bytesUp": 20400, "bytesDown": 3100000 }, … ], // top 20 sites
"costEur": 0.0050,
"pricing": { "eurPerGb": 1, "eurPerHour": 0 }
}| status | Bedeutung |
|---|---|
pending | Created; nobody has connected yet. Counts towards the concurrency limit until it starts or expires. |
active | A browser is running and reporting usage. |
lost | No usage report for 5 minutes. Billed up to the last report; a late report puts it back to active. |
ended | Closed: you disconnected, stopped it, or a limit or the balance ended it. endReason says which. |
expired | Nobody connected within two minutes of creating it. Never billed. |
Der Listenaufruf GET /api/v1/browsers liefert { balanceEur, sessions: [...] } mit denselben Session-Objekten, die neuesten zuerst.
Browser offen halten und neu verbinden
Standardmäßig endet eine Session, sobald Ihr Client die Verbindung trennt. Starten Sie sie mit keepAlive: true, läuft der Browser stattdessen weiter, mit seinen Tabs, Cookies und seiner Exit-IP. So kann ein späteres Skript (oder dasselbe nach einem Absturz oder einem zugeklappten Laptop) dort weitermachen, wo das letzte aufgehört hat:
# a new single-use connect URL for a running keepAlive session (connect within two minutes)
curl -X POST -H "authorization: Bearer cc_live_..." https://www.clearcotelabs.com/api/v1/browsers/<id>/connect- Sie lassen ihn weiterlaufen, indem Sie die Verbindung trennen: mit
browser.close()in Playwright (überconnectOverCDPtrennt das nur die Verbindung), mitbrowser.disconnect()in Puppeteer oder indem Sie einfach Ihren Prozess beenden. - Sie beenden ihn mit
DELETE /api/v1/browsers/<id>oder indem Sie den CDP-BefehlBrowser.closesenden: Das tutbrowser.close()in Puppeteer, in Playwright geht es mitawait (await browser.newBrowserCDPSession()).send("Browser.close"). Bis dahin belegt er einen Platz in Ihrem Concurrency-Limit. - Immer nur ein Client: Ein Reconnect, während ein anderer Client verbunden ist, wird mit
409abgelehnt, ebenso einer für eine Session, die ohnekeepAlivegestartet wurde. - Die Limits gelten auch, während niemand verbunden ist:
idleTimeoutSec(für einen Browser, zu dem Sie zurückkehren wollen, auf bis zu 1800 erhöhen),timeoutSec,maxGbund Ihr Guthaben. Eine offen gelassene Seite lädt weiter ihren Hintergrund-Traffic, der wie jeder andere berechnet wird.
Limits
- 24 gleichzeitig laufende oder startende Browser pro Account.
- Sessions dauern höchstens 4 Stunden.
- Eine Session, die 5 Minuten lang keinen CDP-Befehl erhält, wird geschlossen (einstellbar mit
idleTimeoutSec). - Jede Session startet mit einem frischen Browser-Profil, das beim Ende der Session gelöscht wird – es sei denn, Sie verwenden ein benanntes Profil, das Cookies und Site-Storage über Sessions hinweg behält.
- Aus Sicherheitsgründen kann der Browser keine lokalen Dateien öffnen (
file://), keine Dateien vom Server hochladen, keine privaten oder internen Netze erreichen und keine E-Mails über Port 25 senden. - Datei-Uploads funktionieren aus Playwright:
setInputFiles()sendet die Datei von Ihrem Rechner (bis zu 50 MB).uploadFile()aus Puppeteer wird abgelehnt. Downloads bleiben auf unserem Server und werden mit der Session gelöscht; um eine Datei zu behalten, holen Sie sie aus der Seite heraus ab und geben ihren Inhalt zurück. - Chrome-Erweiterungen lassen sich nicht in gehostete Browser laden.
- Der Browser leitet weder Konsolenmeldungen noch Seitenfehler weiter, daher bleibt
page.on("console")stumm. Sammeln Sie, was Sie brauchen, in der Seite und lesen Sie es mitevaluateaus.
Fehler
| Status | code | Bedeutung |
|---|---|---|
| 400 | — | The body is not JSON, or an option is invalid; the message says which. |
| 400 | UNKNOWN_VERSION | No release matches version; the message lists the ones you can pick. |
| 401 | — | Missing, malformed or revoked API key. |
| 402 | INSUFFICIENT_BALANCE | Balance below the minimum. Top up in the dashboard. |
| 404 | NOT_FOUND | No session with that id on your account. |
| 409 | NOT_RUNNING | Live view or a reconnect asked for before the browser started or after it ended. |
| 409 | NOT_KEEPALIVE | This session cannot be reconnected. Start it with keepAlive: true. |
| 409 | PROFILE_IN_USE | Another session is already saving to that profile. Stop it, or open the profile with persist: false. |
| 429 | CONCURRENCY_LIMIT | Too many browsers running or starting at once. Close one first. |
| 429 | — | More than 60 create calls in a minute from one address. Slow down. |
| 503 | NO_CAPACITY | No free browser slot right now. Retry after a few seconds. |
| 503 | NO_WORKER | The server running that session is not reachable at the moment. |
| 503 | NOT_CONFIGURED | Hosted browsers are not configured on this server. |
| 503 | NOT_AVAILABLE | Notes or profiles are not enabled on this server yet. |
Fehler kommen als JSON: { "error": "...", "code": "..." }. Wird die WebSocket-Verbindung selbst abgelehnt, legen Sie eine neue Session an: Connect-URLs gelten nur einmal und laufen nach zwei Minuten ab. Ein abgelehntes WebSocket-Upgrade antwortet mit einem HTTP-Status und einem JSON-error: 409, wenn die URL bereits verwendet wurde, die Session abgebrochen wurde, nicht mit keepAlive gestartet wurde oder bereits verbunden ist; 401, wenn die URL abgelaufen ist.
Was Sie wiederholen sollten
- Mit Backoff wiederholen:
503 NO_CAPACITYund503 NO_WORKER(1, 2, 4 … Sekunden mit etwas Jitter warten und nach einer Handvoll Versuchen aufgeben) sowie ein429ohne Code (das Rate-Limit pro Adresse). - Einige Male mit Backoff wiederholen: andere
5xx-Antworten sowie ein Connect, der abgelehnt wurde, bevor Ihr Skript gestartet ist (mit einer neuen Session: Die alte Connect-URL ist verbraucht). - Nie in einer Schleife wiederholen:
400(Request korrigieren),401,402(Guthaben aufladen),429 CONCURRENCY_LIMIT(erst einen Browser schließen) und409 PROFILE_IN_USE. Hier muss ein Mensch handeln; Wiederholungen verbrennen nur Requests.
Best Practices
- Verbinden, nie starten. Verwenden Sie
connectOverCDPoderpuppeteer.connectmit derconnectUrl;chromium.launch()startet stattdessen einen Browser auf Ihrem eigenen Rechner. - Nehmen Sie, was schon da ist. Verwenden Sie
browser.contexts()[0]und dessen erste Seite statt eines neuen Kontexts: Ein neuer Kontext startet ohne die Cookies und den Storage des Profils, und Playwright gibt ihm einen emulierten 1280×720-Viewport, der nicht zum Browserfenster passt. - Die Persona beim Anlegen festlegen, nicht im Skript. Land, Zeitzone und Sprache gehören in den Create-Aufruf. Wer User-Agent, Viewport oder navigator-Eigenschaften aus einem Skript überschreibt, erzeugt genau die Inkonsistenzen, nach denen die Erkennung sucht.
- CDP-Hooks eng halten. Breite Listener, das Abfangen jedes Requests und Init-Skripte sind der ganz eigene Fingerprint der Automatisierung. Unverändertes Playwright und Puppeteer funktionieren so, wie sie sind: Die Engine hält die Nebeneffekte von
Runtime.enablevon der Seite selbst fern, sodass ein gepatchter Treiber wie Patchright optional ist statt notwendig. - Eine Session, viele Seiten. Das Starten eines Browsers ist der langsame Teil; navigieren Sie innerhalb der Session. Melden Sie sich einmal mit einem Profil an statt bei jedem Lauf.
- Immer stoppen. Schließen Sie den Browser in einem
finally, und setzen SiemaxGbundidleTimeoutSecpassend zum Job, damit ein Bug keinen Browser auf Kosten Ihres Guthabens weiterlaufen lässt. - Erst hinsehen, dann Code schreiben. Wenn sich eine Site seltsam verhält, beobachten Sie sie in der Live-Ansicht (oder übernehmen Sie die Steuerung), bevor Sie Waits und Workarounds einbauen.
Frameworks
Alles, was sich per CDP an Chrome anhängt, funktioniert mit der connectUrl. Sie gilt nur einmal; ein Framework, das sich selbstständig neu verbindet, braucht daher für jede Verbindung eine neue Session. Der Browser startet, wenn Sie sich verbinden, meist innerhalb weniger Sekunden; vorher muss kein Status abgefragt werden.
// Patchright (optional; a Playwright fork): npm i patchright
import { chromium } from "patchright";
const browser = await chromium.connectOverCDP(connectUrl);
const page = browser.contexts()[0].pages()[0];# Browser Use
from browser_use import Agent, Browser
agent = Agent(task="Find the cheapest flight to Lisbon next Friday", llm=llm, browser=Browser(cdp_url=connect_url))
await agent.run()
# Crawl4AI
from crawl4ai import AsyncWebCrawler, BrowserConfig
config = BrowserConfig(browser_mode="custom", cdp_url=connect_url, use_managed_browser=True)
async with AsyncWebCrawler(config=config) as crawler:
result = await crawler.arun("https://example.com")// Stagehand v4
import { localBrowser, Stagehand } from "@browserbasehq/stagehand";
const stagehand = await Stagehand.create({ browser: await localBrowser.connect({ cdpUrl: connectUrl }) });Ein Helper als Ausgangspunkt
Anlegen mit den Retries von oben, verbinden und immer stoppen – alles in einer Funktion:
// clearcote-hosted.ts
import { chromium, type Browser } from "playwright"; // or "patchright"
const API = "https://www.clearcotelabs.com/api/v1/browsers";
const AUTH = { authorization: "Bearer " + process.env.CLEARCOTE_API_KEY };
const RETRY_CODES = new Set(["NO_CAPACITY", "NO_WORKER"]);
export async function createSession(options: Record<string, unknown> = {}, attempts = 5) {
for (let i = 0; ; i++) {
const res = await fetch(API, {
method: "POST",
headers: { ...AUTH, "content-type": "application/json" },
body: JSON.stringify(options),
});
const body = await res.json().catch(() => ({}));
if (res.ok) return body as { id: string; connectUrl: string; worker: string };
const retry = RETRY_CODES.has(body.code) || (res.status === 429 && !body.code) || [500, 502, 504].includes(res.status);
if (!retry || i + 1 >= attempts) throw new Error([res.status, body.code, body.error].filter(Boolean).join(" "));
await new Promise((r) => setTimeout(r, Math.min(15_000, 1000 * 2 ** i) * (0.5 + Math.random())));
}
}
export async function withBrowser<T>(options: Record<string, unknown>, work: (browser: Browser) => Promise<T>) {
const session = await createSession(options);
try {
const browser = await chromium.connectOverCDP(session.connectUrl);
try {
return await work(browser);
} finally {
await browser.close().catch(() => {});
}
} finally {
// Ends the session if closing the browser did not (a no-op otherwise).
await fetch(API + "/" + session.id, { method: "DELETE", headers: AUTH }).catch(() => {});
}
}
// await withBrowser({ profile: { name: "shop", persist: true }, country: "de" }, async (browser) => {
// const page = browser.contexts()[0].pages()[0];
// await page.goto("https://example.com");
// });Der Playground
Der Playground im Dashboard führt ein Skript gegen einen dieser Browser aus, mit Live-Ansicht, Konsole und Screenshots daneben, und das Panel „Use in your code“ darunter zeigt dieselben Session-Optionen wie der Code oben. Sein page ist ein kleiner Helper, der direkt CDP spricht, nicht Playwright – ein Playground-Skript ist also eine Skizze, die Sie übertragen müssen, keine Datei zum Einfügen:
| Helper | Wirkung |
|---|---|
page.goto(url, { timeout? }) | Navigieren und auf das load-Event warten. |
page.click(sel) · page.type(sel, text) · page.press(key) | Echte Maus- und Tastatureingaben; das Element wird vorher in den sichtbaren Bereich gescrollt. |
page.evaluate(fn, ...args) | Eine Funktion in der Seite ausführen und ihr JSON-Ergebnis zurückbekommen. |
page.waitForSelector(sel, { timeout? }) · page.waitForNavigation() | Auf ein Element warten oder darauf, dass die nächste Seite geladen ist. |
page.scroll(px) · page.screenshot({ fullPage? }) | Mit dem Mausrad scrollen; ein JPEG, das im Tab Screenshots landet. |
page.title() · page.url() · page.content() | Titel, Adresse und HTML des Dokuments. |
log(...values) · sleep(ms) | In die Konsole schreiben (Objekte werden formatiert ausgegeben); pausieren. |
cdp(method, params) | Ein roher CDP-Befehl an den Browser (Target.*, Browser.*, Storage.*). |
page.cdp(method, params) | Ein roher CDP-Befehl an die Seite (Page.*, Runtime.*, DOM.*, Network.*). |
Fehler nennen die Skriptzeile, aus der sie stammen. Share kopiert einen Link, der das Skript und seine Session-Optionen in der URL trägt, bei uns wird also nichts gespeichert; wer den Link öffnet, führt das Skript auf eigenes Guthaben aus.