Back to Articles

chromedp: Driving Chrome with Pure Go and the DevTools Protocol

[ View on GitHub ]

chromedp: Driving Chrome with Pure Go and the DevTools Protocol

Hook

While most browser automation tools add layers of abstraction and external dependencies, chromedp talks directly to Chrome using the same protocol that DevTools uses—and it does it in pure Go without a single external dependency.

Context

Browser automation has long been dominated by Selenium WebDriver, a multi-language framework that requires separate driver binaries (chromedriver, geckodriver) to bridge your code and the browser. This architecture introduces latency, version compatibility headaches, and deployment complexity—especially in containerized environments where you need to bundle both the browser and the driver binary.

When Chrome introduced the DevTools Protocol (CDP) as a standardized way to instrument and control the browser, it opened a new possibility: talking directly to Chrome without middleware. The Node.js ecosystem capitalized on this with Puppeteer, but Go developers were left with Selenium bindings or nothing. chromedp emerged to fill this gap, providing a native Go library that speaks CDP directly, eliminating external processes and dependencies while offering performance improvements and a more idiomatic Go API centered around contexts.

Technical Insight

chromedp's architecture revolves around three core abstractions: contexts, allocators, and actions. The context-first design isn't just a nod to Go conventions—it's fundamental to how the library manages browser lifecycle, timeout propagation, and resource cleanup. Every chromedp operation requires a context that carries both the browser connection and cancellation signals.

The allocator pattern handles browser process management. You create a browser context using chromedp.NewContext(), which by default spins up a local Chrome instance. Here's a minimal example that navigates to a page and extracts text:

package main

import (
    "context"
    "log"
    "time"

    "github.com/chromedp/chromedp"
)

func main() {
    // Create context with timeout
    ctx, cancel := chromedp.NewContext(context.Background())
    defer cancel()

    ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
    defer cancel()

    var title string
    err := chromedp.Run(ctx,
        chromedp.Navigate("https://github.com/chromedp/chromedp"),
        chromedp.WaitVisible("article", chromedp.ByQuery),
        chromedp.Title(&title),
    )
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Page title: %s", title)
}

The chromedp.Run() function accepts a variadic list of actions that execute sequentially. Actions are composable units—navigation, DOM queries, JavaScript execution, screenshot capture—that build on CDP primitives. This action-based API keeps code declarative and testable.

For production scenarios, you often want finer control over browser lifecycle. The ExecAllocator allows you to customize Chrome flags:

opts := append(chromedp.DefaultExecAllocatorOptions[:],
    chromedp.Flag("headless", true),
    chromedp.Flag("disable-gpu", true),
    chromedp.UserAgent("custom-agent/1.0"),
    chromedp.WindowSize(1920, 1080),
)

allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()

ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()

Behind the scenes, chromedp maintains a WebSocket connection to Chrome's debugging endpoint. The companion cdproto package provides generated Go structs for every CDP domain (Page, DOM, Network, etc.). When you call high-level actions like chromedp.Screenshot(), it's orchestrating multiple CDP commands: enabling the Page domain, capturing the screenshot, and decoding the base64 result. You can drop down to raw CDP when needed:

import "github.com/chromedp/cdproto/page"

var pdf []byte
err := chromedp.Run(ctx,
    chromedp.Navigate("https://example.com"),
    chromedp.ActionFunc(func(ctx context.Context) error {
        var err error
        pdf, _, err = page.PrintToPDF().WithPrintBackground(true).Do(ctx)
        return err
    }),
)

This dual-level API is chromedp's secret weapon: high-level convenience with an escape hatch to the full CDP specification when you need it. The context threading means timeouts and cancellations propagate automatically—cancel the parent context and the browser process terminates immediately, no orphaned processes.

For testing scenarios, chromedp integrates naturally with Go's testing package. You can create isolated browser contexts per test, run them in parallel with proper cleanup, and use standard timeout mechanisms. The RemoteAllocator allows connecting to existing Chrome instances, perfect for debugging or distributed test infrastructure where Chrome runs separately from your application.

Gotcha

chromedp's context-based lifecycle management is powerful but has sharp edges. The most common surprise: on Linux systems, Chrome processes automatically terminate when your Go program exits, even if you intended the browser to stay open. This is actually correct behavior—the browser is a child process—but it catches developers off guard when trying to debug by launching Chrome and keeping it running. The solution is using a RemoteAllocator to connect to a separately launched Chrome instance, but this isn't obvious from basic examples.

Error messages can be cryptic, especially "context canceled" and "invalid context" errors. These often indicate you're reusing a canceled context or not properly chaining contexts through your code. The library assumes you understand Go's context semantics deeply—context cancellation propagates immediately to all pending actions, but if you've structured your code incorrectly, the actual error gets masked. Debugging requires understanding both your code's context tree and chromedp's internal state machine.

Headless mode is the default, which seems reasonable for automation but frustrates developers who want to watch the browser during development. You need to explicitly pass chromedp.Flag("headless", false) to see the UI, and many developers waste time wondering why nothing appears on screen. Additionally, Chrome updates can occasionally break chromedp if CDP changes in backward-incompatible ways, though this is rare. Finally, while the library handles most common automation tasks elegantly, complex scenarios like file uploads, handling multiple tabs with interdependencies, or managing extensions require deeper CDP knowledge and more verbose code than you'd write with higher-level tools like Playwright.

Verdict

Use if: You're building production Go services that need browser automation (web scraping, E2E testing, PDF generation, screenshot services), want zero external dependencies for simpler deployments, need precise timeout and cancellation control that integrates with existing Go context patterns, or require maximum performance by eliminating WebDriver overhead. It's especially strong in containerized environments and CI pipelines. Skip if: You're new to browser automation and want hand-holding with visible browser windows by default, need to support multiple browsers beyond Chrome/Chromium, prefer the familiarity of Selenium's widespread patterns and multi-language portability, or want a more debugging-friendly developer experience with built-in trace viewers and inspection tools (consider Rod instead). Also skip if your team isn't comfortable with Go contexts—fighting the framework will be painful.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/ai-agents/chromedp-chromedp.svg)](https://starlog.is/api/badge-click/ai-agents/chromedp-chromedp)