Get 50% off your first month on any monthly plan.

Website Screenshots in Go (Golang): A Developer's Guide

Profile

Written By Hanzala Saleem

Updated At September 03, 2026 | 9 min read

If you have searched for how to screenshot a website in Go, you have probably landed on a chromedp tutorial, tried it, and then hit the part nobody warns you about: keeping a headless Chrome instance alive in production. This guide walks through every practical option, from raw browser automation to a hosted API, with code that actually compiles.

What is the fastest way to take a website screenshot in Go?

The fastest way is a single HTTP GET request to a screenshot API. No browser binary, no driver, no context management, just a URL and a few query parameters.

package main

import (
	"fmt"
	"io"
	"net/http"
	url2 "net/url"
	"os"
)

func main() {
	token := "YOUR_API_KEY"
	target := url2.QueryEscape("https://example.com")

	query := fmt.Sprintf(
		"https://shot.screenshotapi.net/v3/screenshot?token=%s&url=%s&output=image&file_type=png",
		token, target,
	)

	resp, err := http.Get(query)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		body, _ := io.ReadAll(resp.Body)
		panic(fmt.Errorf("screenshot request failed: %s", body))
	}

	file, err := os.Create("screenshot.png")
	if err != nil {
		panic(err)
	}
	defer file.Close()

	if _, err := io.Copy(file, resp.Body); err != nil {
		panic(err)
	}

	fmt.Println("saved screenshot.png")
}

That is the entire integration. If you only need a handful of screenshots or you are prototyping a feature, this is usually the right starting point. If you are building something that runs your own browser fleet, keep reading, because the tradeoffs matter more once you scale past a few requests a day.

What are the main ways to capture website screenshots with Go?

There are three practical categories, and they solve different problems.

Approach Best for Setup Browser required Infrastructure Flexibility
chromedp Go-native browser control, no CGo dependencies go get github.com/chromedp/chromedp Yes, Chrome/Chromium locally You manage it High, but manual
Rod Similar to chromedp with a friendlier chained API go get github.com/go-rod/rod Yes, auto-downloads Chromium You manage it High, but manual
playwright-go (community) Cross-browser testing, not just Chromium go get github.com/playwright-community/playwright-go Yes, plus a Node.js driver bundle You manage it Highest, heaviest
Screenshot API Production apps that just need the image One HTTP call No Fully managed Parameter-driven

Worth noting up front: unlike the Python, Java, and .NET bindings, the Go bindings for Playwright are community maintained rather than an official Microsoft package. That does not make them unusable, but it is a fact that most Go screenshot articles skip, and it matters if you are picking dependencies for a production service.

Can Go capture screenshots using a headless browser?

Yes. Go does not ship a browser, so every browser-based approach in Go works by driving an external Chromium (or Firefox/WebKit) process over the Chrome DevTools Protocol or a similar RPC bridge. chromedp implements the DevTools Protocol directly in Go with no CGo and no external dependencies beyond the browser binary itself, which is why it is the most commonly reached-for library for this task.

How do you take a screenshot with chromedp

Here is a minimal, current example using chromedp's FullScreenshot action, which captures the entire scrollable page rather than just the visible viewport.

package main

import (
	"context"
	"log"
	"os"

	"github.com/chromedp/chromedp"
)

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

	var buf []byte
	if err := chromedp.Run(ctx,
		chromedp.Navigate("https://example.com"),
		chromedp.FullScreenshot(&buf, 90),
	); err != nil {
		log.Fatal(err)
	}

	if err := os.WriteFile("fullScreenshot.png", buf, 0o644); err != nil {
		log.Fatal(err)
	}
}

The 90 argument is the compression quality. When it is set to 100, chromedp encodes the image as PNG; any lower value switches the output to JPEG. If you only want the visible viewport instead of the full page, swap FullScreenshot for CaptureScreenshot, which is documented in the chromedp Go reference.

A couple of details that trip people up in production:

  • chromedp needs a real Chrome or Chromium binary on the host, which means a Docker image built around chromedp/headless-shell or similar, not just go build.
  • chromedp.NewContext alone does not set a timeout. Wrap it with context.WithTimeout or a slow page will hang your goroutine indefinitely.
  • Element-specific captures use chromedp.Screenshot(selector, &buf, chromedp.NodeVisible) instead of FullScreenshot.

What are the limitations of browser-based screenshot generation in Go?

Browser automation gives you full control, but that control comes with operational cost that is easy to underestimate until it shows up in an incident. A single headless Chromium instance typically holds 300 to 500 MB of RAM, and that number climbs fast once you run several in parallel to keep request latency reasonable. Chrome ships security patches on a near-weekly cadence, so a self-hosted fleet needs someone watching CVEs and rebuilding images. Under real traffic, Chrome processes leak memory and occasionally hang, which means you also need process supervision and restart logic, not just a happy-path script.

There is also a rendering-correctness problem specific to modern pages. Lazy-loaded images, infinite scroll, and client-side hydration on React, Vue, or Next.js apps do not automatically populate before a screenshot fires. You end up writing custom scroll-and-wait logic per site, which is exactly the kind of code that breaks silently when a target site changes its layout.

None of this means chromedp or Rod are bad libraries. It means the library is the easy 20% of the problem, and the browser infrastructure is the other 80%.

What are the limitations of browser-based screenshot generation in Go?

Is there an easier way to take website screenshots in Go?

Yes, and it is worth being specific about the tradeoff rather than just calling one option "better." A screenshot API is an HTTP endpoint backed by managed Chromium infrastructure. Your Go code sends a URL and some parameters; the service navigates, renders, waits for the page to settle, and streams back an image or PDF. You give up direct control over the browser process in exchange for not maintaining one.

ScreenshotAPI is built around this model. It runs a real Chromium browser rather than a simplified HTML-to-image renderer, so JavaScript-heavy pages, SPA hydration, and CSS animations render the same way they would in an actual browser tab.

How do you take a website screenshot with ScreenshotAPI in Go?

The endpoint is a plain GET request, which means the standard library is enough. No SDK install required.

package main

import (
	"fmt"
	"io"
	"net/http"
	url2 "net/url"
	"os"
)

func main() {
	// @param token   - your ScreenshotAPI key
	// @param url     - the encoded target URL
	// @param output  - "image" or "json"
	// @param file_type - "png", "jpg", or "webp"
	token := "Your API Key"
	target := url2.QueryEscape("https://example.com")
	output := "image"
	fileType := "png"

	query := "https://shot.screenshotapi.net/v3/screenshot"
	query += fmt.Sprintf("?token=%s&url=%s&output=%s&file_type=%s", token, target, output, fileType)

	resp, err := http.Get(query)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		errorBody, _ := io.ReadAll(resp.Body)
		panic(fmt.Errorf("error calling api: %s", errorBody))
	}

	file, err := os.Create("./screenshot.png")
	if err != nil {
		panic(err)
	}
	defer file.Close()

	if _, err := io.Copy(file, resp.Body); err != nil {
		panic(err)
	}

	fmt.Println("screenshot saved")
}

You will find the same base pattern in the Get Started docs for Node.js, Python, PHP, Java, and Ruby, since every language integration is the same GET request with different HTTP client syntax. Your API key comes from the dashboard after signup, and the free tier includes 100 screenshots with no card required.

How can you customize screenshots from Go?

Every option is a query parameter on the same endpoint, so customizing output means adding key-value pairs to the url2.Values map above. A few that come up constantly:

Parameter What it does
full_page=true Captures the entire scrollable page instead of one viewport
width / height Sets viewport dimensions in pixels
retina=true Renders at 2x pixel density
dark_mode=true Renders the page using prefers-color-scheme: dark
block_ads=true Strips ads using a 20,000+ rule blocklist
no_cookie_banners=true Removes GDPR/consent popups before capture
selector=#id Screenshots one element instead of the full page
output=pdf Returns a PDF instead of an image

Full parameter documentation, including file type and quality controls, lives in the rendering docs and the viewport and full page docs.

How do you capture full-page screenshots in Go?

With chromedp, this means calling FullScreenshot instead of CaptureScreenshot, as shown earlier. The tradeoff is that chromedp will scroll and capture whatever has loaded at the moment it fires, so lazy-loaded sections below the fold can come out blank unless you add manual scroll-and-wait logic first.

With ScreenshotAPI, setting full_page=true handles this on the server side: the renderer scrolls the page top to bottom before capturing, which also triggers IntersectionObserver-based lazy loading along the way. This is the same underlying idea, just moved out of your Go code and into the rendering service.

params := url2.Values{}
params.Set("output", "image")
params.Set("file_type", "png")
params.Set("full_page", "true")
params.Set("lazy_load", "true")

How do you handle dynamic websites and single-page apps?

This is where browser-based and API-based approaches converge on the same underlying strategy: wait for the right signal before capturing, not just for a fixed delay.

For chromedp, that usually means combining chromedp.WaitVisible with a CSS selector that only appears once your target content has rendered, rather than sleeping for a guessed number of milliseconds.

For ScreenshotAPI, the same idea maps to three parameters documented under lazy loading and delay:

  • delay (milliseconds) for animation-heavy pages that need idle time after load.
  • wait_for_selector to hold the capture until a specific DOM node exists.
  • wait_for_event=networkidle to wait until there have been no active network requests for roughly 500ms, which is a good default for React, Vue, and other SPA frameworks that fetch data after the initial render.
params.Set("wait_for_event", "networkidle")
params.Set("lazy_load", "true")

Combining wait_for_event=networkidle with lazy_load=true covers most SPA rendering issues without per-site tuning.

When should you use a screenshot API instead of browser automation?

Situation Recommended approach
One-off script, internal tool, low volume chromedp or Rod, either works fine
Production service with unpredictable traffic Screenshot API, avoids scaling your own Chrome fleet
CI/CD visual regression testing Screenshot API, consistent Chromium version across every run
Need cross-browser (Firefox, WebKit) coverage playwright-go, accept the community-maintenance tradeoff
Scheduled monitoring, competitor tracking, archiving Screenshot API, built-in cron scheduling and cloud storage
Deep custom DOM manipulation before capture chromedp or Rod, you need full page control

If your Go service already has infrequent, predictable screenshot needs and a team comfortable maintaining Docker images with Chrome inside them, self-hosting is a reasonable choice. If screenshots are a small part of a larger product and you would rather not own browser infrastructure, an API removes that entire category of operational work.

When should you use a screenshot API instead of browser automation?

Best practices for website screenshots in Go

  • Always set a timeout, whether it is context.WithTimeout around a chromedp context or http.Client{Timeout: ...} around an API call. Slow or hung pages should not block your goroutines indefinitely.
  • URL-encode the target URL with net/url.QueryEscape before building any query string. Unescaped query parameters in the target URL will break the outer request.
  • Cache screenshots you do not need fresh on every request. ScreenshotAPI caches by default and exposes a fresh=true parameter to bypass it when you specifically need a current capture.
  • For CI/CD visual regression, pin the same rendering environment for every run. If you self-host, that means pinning a specific Chrome version in your Docker image; with an API, the provider handles version consistency for you.
  • Store screenshots outside your application server. Write directly to S3, GCS, or another bucket rather than holding large image buffers in memory longer than necessary.

Conclusion

Go does not have a built-in way to screenshot a webpage, so every option runs through either a locally driven Chromium process or a remote rendering service. chromedp and Rod give you full control at the cost of infrastructure you have to build and patch yourself. A screenshot API trades that control for a single HTTP call, which is usually the better fit once screenshots move from a side script to something running in production. Try the direct HTTP approach first since it takes minutes to wire up, and drop down to chromedp only if you find yourself needing page interaction the API's parameters cannot express.

Frequently Asked Questions

Can Go take screenshots of websites?

Yes. Go has no native screenshot capability, but libraries like chromedp and Rod drive a real Chromium browser over the DevTools Protocol to capture pages. Alternatively, a screenshot API handles the browser side entirely and returns the image over a normal HTTP response.

How do you take a full-page screenshot in Go without cutting off content?

With chromedp, use chromedp.FullScreenshot instead of CaptureScreenshot, which captures the full scrollable page rather than just the viewport. With a screenshot API, set full_page=true, which scrolls the page server-side before capturing so lazy-loaded sections are not left blank.

Do I need to install Chrome to take screenshots in Go?

Only if you are using a browser automation library like chromedp, Rod, or playwright-go, all of which need a local Chromium binary (or the driver bundle that installs one). If you use a hosted screenshot API instead, your Go code never touches a browser directly, so no local Chrome install is required.