chromedp, rod, and the API approach — with working code.
Go doesn't have a Playwright equivalent with the same polish as the Node.js ecosystem, but there are solid options for headless browser automation. Here are three approaches to taking screenshots in Go, each with different trade-offs.
chromedp is the most popular Go library for driving Chrome via the DevTools Protocol. It's low-level — closer to Puppeteer's API than Playwright's.
Installgo get github.com/chromedp/chromedp
main.go
package main
import (
"context"
"os"
"time"
"github.com/chromedp/chromedp"
)
func main() {
// Create context with timeout
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
var buf []byte
err := chromedp.Run(ctx,
chromedp.EmulateViewport(1440, 900),
chromedp.Navigate("https://example.com"),
chromedp.WaitReady("body"),
chromedp.CaptureScreenshot(&buf),
)
if err != nil {
panic(err)
}
os.WriteFile("screenshot.png", buf, 0644)
}
chromedp requires Chrome/Chromium installed on the system. It connects via the DevTools Protocol — it doesn't bundle a browser like Playwright does.
import "github.com/chromedp/cdproto/page"
// Full page screenshot requires using the CDP directly
var buf []byte
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.WaitReady("body"),
chromedp.ActionFunc(func(ctx context.Context) error {
// Get full page metrics
_, _, _, _, _, contentSize, err := page.GetLayoutMetrics().Do(ctx)
if err != nil { return err }
// Set viewport to full page height
chromedp.EmulateViewport(
int64(contentSize.Width),
int64(contentSize.Height),
).Do(ctx)
// Capture
buf, err = page.CaptureScreenshot().
WithFormat(page.CaptureScreenshotFormatPng).
WithCaptureBeyondViewport(true).
Do(ctx)
return err
}),
)
rod is a higher-level alternative to chromedp. It manages the browser lifecycle automatically and has a more ergonomic API.
go get github.com/go-rod/rod
package main
import "github.com/go-rod/rod"
func main() {
page := rod.New().MustConnect().MustPage("https://example.com")
page.MustWindowFullscreen()
page.MustWaitStable()
page.MustScreenshot("screenshot.png")
}
rod auto-downloads a compatible browser binary (like Playwright). The Must* methods panic on error — use the non-Must variants for production code.
The simplest approach — no browser to manage, just HTTP:
package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
"os"
)
func main() {
body, _ := json.Marshal(map[string]any{
"url": "https://example.com",
"width": 1440,
"height": 900,
})
req, _ := http.NewRequest("POST",
"https://api.nightglass.xyz/api/v1/screenshot",
bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
f, _ := os.Create("screenshot.png")
defer f.Close()
io.Copy(f, resp.Body)
}
No Chrome to install, no binary management, works in any environment including serverless. At $0.005 per screenshot, it's the cheapest approach when you factor in the cost of managing browser infrastructure.
| Approach | Best for | Downsides |
|---|---|---|
| chromedp | Full browser control, complex interactions | Low-level API, requires Chrome installed |
| rod | Simpler API, auto-downloads browser | Less community support than chromedp |
| Screenshot API | URL-to-image without infra | External dependency, costs per call |
For most Go applications that just need URL-to-image, the API approach is the path of least resistance. Note: if deploying to serverless, running Chrome isn't possible — see why. For a broader overview, the complete headless browser guide covers architecture and when self-hosting makes sense. Comparing APIs? 2026 API comparison.