Unser Interesse: wir sind Geonode und verkaufen Proxys, und Browser-Automatisierung ist das bandbreitenhungrigste, was Sie darüber tun können. Ein Headless-Browser holt jedes Bild, jede Schrift, jedes Skript und jedes Video-Preload, deshalb kostet chromedp über getakteten Traffic grob eine Größenordnung mehr als rohe HTTP-Requests für dieselben Seiten. Es gibt einen Abschnitt zum Kürzen, und die Technik darin spart mehr, als ein günstigerer Anbieter. Die Proxy-Konfiguration selbst sind drei Zeilen und hat eine echte Falle, die ebenfalls behandelt wird.
Was chromedp ist
Das Projekt beschreibt sich als „a faster, simpler way to drive browsers supporting the Chrome DevTools Protocol in Go without external dependencies“.
Dieser letzte Satzteil ist das Verkaufsargument. Selenium braucht ein Driver-Binary passend zur Browserversion; Playwright liefert eine eigene Runtime. chromedp spricht das DevTools Protocol direkt über einen Websocket, also sind ein Go-Binary plus eine Chrome-Installation das gesamte Deployment.
Es ist MIT-lizenziert und aktiv gepflegt — Version 0.15.1 erschien im April 2026, mit Commits bis Juli, geprüft im September 2026.
Die Installation ist unspektakulär:
go get -u github.com/chromedp/chromedp
Die generierten Protokoll-Bindings liegen im Begleitpaket github.com/chromedp/cdproto, das Sie holen, wenn die High-Level-API etwas nicht einpackt.
Das Context-Modell
Der Teil, den man zuerst verstehen muss, weil alles andere daraus folgt.
chromedp nutzt context.Context für zwei Jobs zugleich: Abbruch, wie Go das immer tut, und das Tragen der Browser- und Tab-Handles. Dieser Doppelzweck erklärt, warum das Setup so aussieht.
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var title string
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.Text("h1", &title, chromedp.NodeVisible),
)
Das erste NewContext allokiert einen Browser. Nachfolgende, davon abgeleitete Contexts erzeugen neue Tabs im selben Browser — so laufen mehrere Seiten, ohne Startkosten wiederholt zu zahlen:
browserCtx, cancelBrowser := chromedp.NewContext(context.Background())
defer cancelBrowser()
tabCtx, cancelTab := chromedp.NewContext(browserCtx)
defer cancelTab()
Abbrechen schließt Dinge. Einen Tab-Context abbrechen schließt den Tab; den Browser-Context abbrechen schließt den Browser. defer cancel() ist keine optionale Buchhaltung — weglassen leakt einen Chrome-Prozess.
Timeouts komponieren sich auf die gewöhnliche Go-Weise:
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
Zwei Fehler aus dem FAQ des Projekts sollte man vorher kennen.
„Executing an action without Run results in 'invalid context'.“ Das FAQ erklärt, „by default, a chromedp context does not have an executor, however one can be specified manually if necessary“. Actions führen sich nicht selbst aus — sie sind Werte, die Run ausführt.
„I'm seeing 'context canceled' errors.“ Das FAQ führt das auf Verbindungsverlust zurück: „when the connection to the browser is lost, chromedp cancels the context, and it may result in this error. This occurs, for example, if the browser is closed manually, or if the browser process has been killed or otherwise terminated.“ Ein context canceled-Fehler bedeutet also häufig, dass Chrome gestorben ist, nicht dass der Timeout gefeuert hat — das sollte man unterscheiden, bevor man den Timeout hochsetzt.
Actions
Run nimmt eine Folge von Actions und führt sie der Reihe nach aus. Die gängigen decken die meiste Arbeit ab.
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com/search"),
chromedp.WaitVisible(`input[name="q"]`),
chromedp.SendKeys(`input[name="q"]`, "golang"),
chromedp.Click(`button[type="submit"]`, chromedp.NodeVisible),
chromedp.WaitVisible(`.results`),
chromedp.Text(`.results`, &results, chromedp.NodeVisible),
)
Aus mehreren Elementen extrahieren nutzt Nodes oder Evaluate:
var links []string
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.Evaluate(`[...document.querySelectorAll('a')].map(a => a.href)`, &links),
)
Evaluate führt JavaScript in der Seite aus und unmarshallt das Ergebnis in einen Go-Wert; das ist oft der kürzeste Weg für alles, was mehrere Elemente auf einmal betrifft. Der Wert muss JSON-serialisierbar sein.
Für Actions, die mehrere Werte zurückgeben, gibt das FAQ den Wrapper:
chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error {
_, err := domain.SomeAction().Do(ctx)
return err
}))
ActionFunc ist auch der Weg in rohe cdproto-Aufrufe für alles, was die High-Level-API nicht abdeckt — Cookies setzen, Netzwerkanfragen abfangen, ein Gerät emulieren. Diese Luke gilt für das gesamte DevTools Protocol, eine große Fläche.
Richtig warten
Der Unterschied zwischen einem zuverlässigen Scraper und einem wackligen, und der Fehler ist immer derselbe.
Nicht schlafen. chromedp.Sleep(3*time.Second) existiert, ist verlockend und entweder zu kurz — intermittierende Fehler an einem langsamen Tag — oder zu lang und verschwendet Zeit bei jedem Lauf. Meist beides, auf verschiedenen Maschinen.
Warten Sie auf das, was Sie interessiert:
chromedp.WaitVisible(`.results`, chromedp.ByQuery)
chromedp.WaitNotVisible(`.spinner`)
chromedp.WaitReady(`#content`)
WaitVisible wartet, bis das Element existiert und sichtbar ist; WaitReady wartet auf Existenz im DOM. Für Inhalt nach einer Interaktion ist sichtbar meist die richtige Bedingung.
Für eine Bedingung, die kein Selektor ausdrückt, pollen Sie in der Seite:
chromedp.Poll(`document.querySelectorAll('.item').length >= 20`, nil)
Das ist die Antwort auf „warte, bis die Liste fertig geladen ist“, was kein elementbasiertes Warten ausdrücken kann.
Begrenzen Sie das Warten immer mit einem Context-Timeout. Ein WaitVisible auf einem Selektor, der nie matcht, blockiert bis der Context abläuft — ohne Timeout für immer.
Headless laufen, und in Docker
Chrome läuft standardmäßig headless. Das FAQ beantwortet die erste Frage: „By default, Chrome is run in headless mode. See DefaultExecAllocatorOptions, and an example to override the default options.“
Um beim Entwickeln zuzusehen:
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", false),
)
allocCtx, cancelAlloc := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancelAlloc()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
NewExecAllocator ist der Ort für Chromes Kommandozeilen-Flags und die Schicht über dem Browser-Context.
Für Container ist die Projektempfehlung konkret: „The simplest way is to run the Go program that uses chromedp inside the chromedp/headless-shell image. That image contains headless-shell, a smaller headless build of Chrome, which chromedp is able to find out of the box.“
Das sollte man annehmen. Ein funktionierendes Chrome in einem Container von Hand zusammenzubauen heißt, fehlende Shared Libraries und Font-Pakete zu jagen, und das Ergebnis ist größer als das zweckgebaute Image.
Ein Linux-spezifisches Verhalten aus dem FAQ überrascht Leute, die Chrome getrennt laufen lassen: „On Linux, chromedp is configured to avoid leaking resources by force-killing any started Chrome child processes. If you need to launch a long-running Chrome instance, manually start Chrome and connect using RemoteAllocator.“
RemoteAllocator verbindet sich mit einem bereits laufenden Browser über dessen Websocket-Endpunkt — das Muster für einen geteilten Browser-Pool oder einen Browser in einem separaten Container.
Einen Proxy nutzen
Drei Zeilen und eine Falle.
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.ProxyServer("http://proxy.example.com:9000"),
)
allocCtx, cancelAlloc := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancelAlloc()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
ProxyServer setzt Chromes Flag --proxy-server.
Die Falle ist Authentifizierung. Chromes Flag --proxy-server nimmt keine Credentials — eine URL mit Benutzername und Passwort authentifiziert nicht. Chrome reagiert auf eine Proxy-Challenge mit einem Dialog, und ein Headless-Browser hat niemanden, der ihn ausfüllt.
Zwei Auswege, in bevorzugter Reihenfolge.
Eine IP-Allowlist nutzen. Ist die Quelladresse stabil, beim Anbieter hinterlegen und die Credentials ganz weglassen. Das ist die sauberste Antwort und entfernt das Problem statt es zu umgehen.
Das Authentifizierungsereignis behandeln. chromedp kann auf die Authentifizierungsanfrage des DevTools Protocol über fetch.Enable mit handleAuthRequests antworten und Credentials programmatisch liefern. Das ist mehr Code und der Weg, wenn die Adresse nicht fest ist.
Dann prüfen, ob es geklappt hat, denn ein falsch konfigurierter Proxy im Browser ist still:
var ip string
err := chromedp.Run(ctx,
chromedp.Navigate("https://api.ipify.org"),
chromedp.Text("body", &ip, chromedp.NodeVisible),
)
Mit und ohne Proxy-Option laufen lassen. Ändert sich die Adresse nicht, nutzt Chrome sie nicht — und nichts hat Sie gewarnt. Für geo-gezielte Arbeit gehen Sie weiter und bestätigen, dass regional unterschiedlicher Inhalt tatsächlich differiert; die Adresse ist der leichte Teil. Das ist das Silent-Failure-Muster aus warum Proxy-Tests wichtig sind.
Passen Sie das Locale an das Exit-Land an, während Sie dabei sind. Ein deutscher Exit mit en-US-Sprachheader und Londoner Zeitzone ist eine Kombination, die kein echter Besucher erzeugt, und viele Sites nutzen Locale unabhängig von der Adresse:
chromedp.Flag("lang", "de-DE"),
Bandbreite kürzen
Der Abschnitt, der am meisten Geld spart, und er gilt für jede Browser-Automatisierung.
Eine Seite mit 200 KB HTML kann 4 MB werden, sobald jedes Bild, jede Schrift, jedes Tracking-Skript und jedes Video-Preload geholt wurde. Bei Residential-Tarifen von 0,79 $/GB — unsere Zahlen, geprüft gegen die Preisseite im September 2026 — ist diese Differenz das ganze Budget.
Ressourcentypen blockieren, die Sie nicht brauchen. Mit fetch.Enable und einem Request-paused-Listener Anfragen für Bilder, Medien und Schriften abbrechen:
chromedp.ListenTarget(ctx, func(ev interface{}) {
if e, ok := ev.(*fetch.EventRequestPaused); ok {
go func() {
c := chromedp.FromContext(ctx)
execCtx := cdp.WithExecutor(ctx, c.Target)
switch e.ResourceType {
case network.ResourceTypeImage, network.ResourceTypeMedia, network.ResourceTypeFont:
_ = fetch.FailRequest(e.RequestID, network.ErrorReasonBlockedByClient).Do(execCtx)
default:
_ = fetch.ContinueRequest(e.RequestID).Do(execCtx)
}
}()
}
})
Das schneidet den Traffic routinemäßig um den Großteil und beschleunigt Läufe als Nebeneffekt.
Browser wiederverwenden, Tabs erzeugen. Browser-Start ist teuer; ein Tab ist billig. Für einen Lauf über viele Seiten einmal allokieren und Tab-Contexts ableiten.
Und fragen, ob Sie überhaupt einen Browser brauchen. Steht der Inhalt im initialen HTML, kostet ein schlichter HTTP-Request einen Bruchteil und läuft viel schneller. Prüfen Sie den Seitenquelltext, bevor Sie zur Automatisierung greifen — der Reflex, alles zu rendern, ist die häufigste Quelle unnötiger Kosten in diesem Bereich.
Debuggen, wenn nichts geht
Browser-Automatisierung scheitert undurchsichtig — ein Selektor, der nie matcht, und eine Seite, die nie lädt, erzeugen denselben Timeout. Eine feste Sequenz löst das meiste.
Browser einschalten und zuschauen. Die schnellste Diagnose, und die, die Leute zuletzt machen:
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", false),
)
In der Hälfte der Fälle ist die Antwort sofort sichtbar — ein Cookie-Banner über dem Button, ein Redirect zur Login-Seite, ein Challenge-Screen oder ein Layout, das vom Getesteten abweicht.
Die Seite bei einem Fehlschlag erfassen. In einer Headless-Umgebung ersetzt das das Zuschauen:
var buf []byte
_ = chromedp.Run(ctx, chromedp.FullScreenshot(&buf, 90))
_ = os.WriteFile("failure.png", buf, 0644)
Kombinieren Sie es mit dem HTML: ein Screenshot zeigt, was gerendert wurde, der Quelltext, was ankam:
var html string
_ = chromedp.Run(ctx, chromedp.OuterHTML("html", &html, chromedp.ByQuery))
Prüfen, ob der Selektor überhaupt auflöst, bevor Sie ein Timing-Problem annehmen:
var count int
_ = chromedp.Run(ctx, chromedp.Evaluate(`document.querySelectorAll('.item').length`, &count))
Null bedeutet ein Selektorproblem, und Warten behebt das nicht.
Browser-Logging aktivieren, wenn Sie vermuten, dass die Seite selbst Fehler wirft:
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("enable-logging", true),
chromedp.Flag("v", "1"),
)
Auf Konsolenmeldungen und fehlgeschlagene Requests hören, die oft eine leer gerenderte Seite erklären:
chromedp.ListenTarget(ctx, func(ev interface{}) {
switch e := ev.(type) {
case *runtime.EventConsoleAPICalled:
log.Printf("console.%s", e.Type)
case *network.EventLoadingFailed:
log.Printf("failed: %s %s", e.Type, e.ErrorText)
}
})
Eine Seite, deren API-Aufrufe alle scheitern, sieht identisch aus wie eine, deren Selektoren sich geändert haben; nur die Netzwerkevents unterscheiden sie.
Und chromedp-proxy als letzten Ausweg nutzen. Es sitzt zwischen Programm und Browser und loggt den DevTools-Protocol-Traffic in beide Richtungen. Wenn Verhalten gar keinen Sinn ergibt, erklärt der tatsächliche Protokollaustausch es meist in einem Lesen.
Wann chromedp die richtige Wahl ist
Nutzen Sie es, wenn Sie bereits Go schreiben und ein einziges statisches Binary ohne zu verteilenden Driver wollen, oder wenn Sie direkten DevTools-Protocol-Zugriff für etwas brauchen, das die höheren Tools nicht exponieren.
Erwägen Sie Playwright, wenn Sie Cross-Browser-Support, Auto-Waiting in jeder Action, Tracing und Screenshots bei Fehlschlag oder einen größeren Bestand an Dokumentation und Beispielen wollen. Der Go-Port existiert, das Ökosystem zentriert sich aber auf JavaScript und Python.
Erwägen Sie schlichtes HTTP, wenn der Inhalt in der initialen Antwort steckt. Schneller, billiger, einfacher — und mehr vom Web liefert nützliches HTML, als der Diskurs nahelegt.
Die Ressourcenliste des FAQ ist eine gute Karte für den nächsten Schritt: das examples-Repository für komplexe Actions und Vollseiten-Screenshots, die cdproto-Referenz für die generierte Protokoll-API und chromedp-proxy — ein CDP-Logging-Proxy — um genau zu sehen, was Programm und Browser einander sagen; das Debugging-Werkzeug letzter Instanz und ein wirklich gutes.
Häufige Fragen
Was ist chromedp?
Ein Go-Paket, das Browser steuert, die das Chrome DevTools Protocol sprechen, ohne externe Abhängigkeiten. Anders als Selenium braucht es kein Driver-Binary, anders als Playwright liefert es keine Runtime — ein Go-Binary plus Chrome-Installation ist das gesamte Deployment.
Warum bekomme ich „invalid context“ in chromedp?
Weil Sie eine Action ohne Run ausgeführt haben. Das FAQ erklärt, dass ein chromedp-Context standardmäßig keinen Executor hat. Actions sind Werte, die chromedp.Run ausführt; ein direkter Aufruf hat nichts, das sie ausführt.
Was bedeutet „context canceled“ in chromedp?
Meist, dass die Browserverbindung verloren ging. Das FAQ führt es auf manuelles Schließen oder getöteten Prozess zurück. Das vom Timeout zu unterscheiden lohnt, weil der Fix anders ist — ein abgestürztes Chrome löst man nicht durch längeres Warten.
Wie starte ich chromedp mit sichtbarem Browser?
Chrome läuft standardmäßig headless. Hängen Sie chromedp.Flag("headless", false) an DefaultExecAllocatorOptions und übergeben Sie sie an NewExecAllocator, dann leiten Sie den Context von diesem Allocator ab.
Wie nutze ich einen Proxy mit chromedp?
Fügen Sie chromedp.ProxyServer("http://host:port") zu den Allocator-Optionen hinzu. Credentials in der URL funktionieren nicht, weil Chromes Flag --proxy-server sie nicht akzeptiert — nutzen Sie eine IP-Allowlist, wenn die Adresse stabil ist, oder behandeln Sie die Authentifizierungsanfrage über das DevTools Protocol.
Wie führe ich chromedp in Docker aus?
Lassen Sie das Go-Programm im Image chromedp/headless-shell laufen, das das Projekt ausdrücklich empfiehlt. Es enthält einen kleineren Headless-Chrome-Build, den chromedp ohne Konfiguration findet, und erspart das Zusammenbauen einer Browserumgebung von Hand.
Wie warte ich in chromedp auf ein Element?
Nutzen Sie WaitVisible, WaitReady oder WaitNotVisible mit einem Selektor, oder Poll mit einem JavaScript-Ausdruck für Bedingungen, die kein Selektor ausdrückt. Vermeiden Sie Sleep — zu kurz und wacklig oder zu lang und verschwenderisch, meist beides je nach Maschine.
Wie reduziere ich Bandbreite mit chromedp?
Blockieren Sie Bilder, Schriften und Medien, indem Sie Request-Interception aktivieren und diese Ressourcentypen fehlschlagen lassen; das entfernt typischerweise den Großteil des Traffics. Wiederverwenden Sie einen Browser und erzeugen Sie Tabs statt wiederholt zu allokieren. Und prüfen Sie, ob der Inhalt im initialen HTML steht — dann den Browser ganz weglassen.
Fazit
Die Lernkurve von chromedp ist fast ganz das Context-Modell. Sobald Sie verinnerlicht haben, dass ein Context den Browser oder Tab trägt, dass Abbrechen ihn schließt und dass Actions nichts tun, bis Run sie ausführt, ist der Rest der API geradlinig.
Die Gewohnheiten, die zählen, sind dieselben wie in jeder Browser-Automatisierung. Auf Bedingungen warten statt schlafen, jedes Warten mit einem Context-Timeout begrenzen und einen Browser über viele Tabs wiederverwenden statt Startkosten wiederholt zu zahlen.
Zwei Go-spezifische Punkte sind merkwürdig. defer cancel() auf jedem Context, sonst leaken Chrome-Prozesse — und unter Linux force-killt chromedp die Chrome-Kinder, die es gestartet hat, also muss ein langlebiger Browser separat gestartet und mit RemoteAllocator erreicht werden.
Und wenn Sie über einen getakteten Proxy laufen, blockieren Sie unnötige Ressourcentypen, bevor Sie sonst etwas tun. Eine gerenderte Seite kostet eine Größenordnung mehr als das HTML darin, und das meiste davon sind Bilder, die Sie nie ansehen würden.
