Menu
golangscreenshotsapi

Website Screenshots in Go: chromedp vs Screenshot API

SnapSharp Team·April 14, 2026·6 min read

Taking a screenshot of a URL in Go sounds straightforward until you try it. The standard approach — running a headless Chromium via chromedp or rod — works locally but falls apart fast in production: binary dependencies, ~300 MB Docker images, memory leaks under concurrent load, and Chromium version drift between environments.

This article covers both paths. You will see what the local browser approach actually looks like at the code level, understand where it breaks, and then see how to replace 60+ lines of infrastructure code with a 5-line HTTP call to the SnapSharp screenshot API.


The chromedp Approach

chromedp is the most popular Go library for driving headless Chrome. Here is a minimal working example:

package main

import (
	"context"
	"log"
	"os"
	"time"

	"github.com/chromedp/chromedp"
)

func screenshotWithChromedp(targetURL string) ([]byte, error) {
	opts := append(chromedp.DefaultExecAllocatorOptions[:],
		chromedp.Flag("headless", true),
		chromedp.Flag("no-sandbox", true),
		chromedp.Flag("disable-gpu", true),
		chromedp.Flag("disable-dev-shm-usage", true),
		chromedp.WindowSize(1280, 720),
	)

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

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

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

	var buf []byte
	err := chromedp.Run(ctx,
		chromedp.Navigate(targetURL),
		chromedp.WaitVisible("body", chromedp.ByQuery),
		chromedp.FullScreenshot(&buf, 90),
	)
	if err != nil {
		return nil, err
	}
	return buf, nil
}

func main() {
	data, err := screenshotWithChromedp("https://example.com")
	if err != nil {
		log.Fatal(err)
	}
	os.WriteFile("screenshot.png", data, 0644)
}

That is already 50+ lines for a basic case. Before this runs in production you also need to:

  • Install google-chrome-stable or chromium-browser on the host (or base your Docker image on zenika/alpine-chrome)
  • Pin the Chrome version and keep it in sync with chromedp
  • Allocate shared memory (--shm-size=2g in Docker or --disable-dev-shm-usage)
  • Handle pool management yourself if you need concurrent captures
  • Deal with context deadline exceeded errors when a page is slow or blocks on JS

rod has a cleaner API and auto-downloads Chrome, but the same fundamental constraints apply: a Chrome binary lives in your container, and you own all of its operational issues.

When chromedp/rod makes sense: local CLI tools, one-off scripts, environments where you already manage a Chrome binary and concurrency is not a concern.

When it does not: containerized microservices, serverless functions (Lambda, Cloud Run), CI/CD pipelines where you want a lean image, or any scenario with burst traffic.


The API Approach: SnapSharp

The SnapSharp API runs Chromium in the cloud and returns the screenshot over HTTP. Your Go service makes one HTTP request and gets back bytes. No binary, no pool management, no memory allocation for browser processes.

1. Simple Screenshot via net/http

No SDK required. A plain GET request with query parameters:

package main

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

func takeScreenshot(targetURL, apiKey string) ([]byte, error) {
	params := url.Values{}
	params.Set("url", targetURL)
	params.Set("width", "1280")
	params.Set("height", "720")
	params.Set("format", "png")

	req, err := http.NewRequest(http.MethodGet,
		"https://api.snapsharp.dev/v1/screenshot?"+params.Encode(), nil)
	if err != nil {
		return nil, fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("do request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("api error %d: %s", resp.StatusCode, body)
	}

	return io.ReadAll(resp.Body)
}

func main() {
	data, err := takeScreenshot("https://example.com", "sk_live_YOUR_API_KEY")
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}
	if err := os.WriteFile("screenshot.png", data, 0644); err != nil {
		fmt.Fprintf(os.Stderr, "write file: %v\n", err)
		os.Exit(1)
	}
	fmt.Printf("saved %d bytes\n", len(data))
}

That is the entire implementation. The API key comes from the Authorization header, exactly like every other REST API your Go service already talks to.

2. POST Request with Advanced Options

For full-page captures, dark mode, and custom viewport, use a POST with a JSON body:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
)

type ScreenshotRequest struct {
	URL       string `json:"url"`
	Width     int    `json:"width"`
	FullPage  bool   `json:"full_page"`
	DarkMode  bool   `json:"dark_mode"`
	Format    string `json:"format"`
	BlockAds  bool   `json:"block_ads"`
	CacheTTL  int    `json:"cache_ttl"`
}

func takeScreenshotPOST(params ScreenshotRequest, apiKey string) ([]byte, error) {
	body, err := json.Marshal(params)
	if err != nil {
		return nil, fmt.Errorf("marshal params: %w", err)
	}

	req, err := http.NewRequest(http.MethodPost,
		"https://api.snapsharp.dev/v1/screenshot",
		bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("do request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("api error %d: %s", resp.StatusCode, b)
	}

	return io.ReadAll(resp.Body)
}

func main() {
	data, err := takeScreenshotPOST(ScreenshotRequest{
		URL:      "https://github.com",
		Width:    1440,
		FullPage: true,
		DarkMode: true,
		Format:   "webp",
		BlockAds: true,
		CacheTTL: 3600,
	}, "sk_live_YOUR_API_KEY")
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}
	if err := os.WriteFile("screenshot.webp", data, 0644); err != nil {
		fmt.Fprintf(os.Stderr, "write: %v\n", err)
		os.Exit(1)
	}
	fmt.Printf("full-page screenshot: %d bytes\n", len(data))
}

The cache_ttl field tells the API to cache the result for 1 hour. Repeated calls to the same URL within that window return instantly from cache — useful if you are generating report thumbnails on a schedule.

3. Saving to File with Context and Timeout

Production services should always pass a context.Context for timeout and cancellation propagation:

package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

func saveScreenshot(ctx context.Context, targetURL, outputPath, apiKey string) error {
	params := url.Values{}
	params.Set("url", targetURL)
	params.Set("width", "1280")
	params.Set("format", "png")
	params.Set("full_page", "true")
	params.Set("wait_until", "networkidle")

	req, err := http.NewRequestWithContext(ctx, http.MethodGet,
		"https://api.snapsharp.dev/v1/screenshot?"+params.Encode(), nil)
	if err != nil {
		return fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("api returned %d: %s", resp.StatusCode, b)
	}

	f, err := os.Create(outputPath)
	if err != nil {
		return fmt.Errorf("create file: %w", err)
	}
	defer f.Close()

	n, err := io.Copy(f, resp.Body)
	if err != nil {
		return fmt.Errorf("write file: %w", err)
	}

	fmt.Printf("saved %d bytes to %s\n", n, outputPath)
	return nil
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	err := saveScreenshot(ctx,
		"https://snapsharp.dev",
		"snapsharp-homepage.png",
		"sk_live_YOUR_API_KEY",
	)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}
}

The wait_until=networkidle parameter tells the renderer to wait until the page has no pending network requests before capturing — the equivalent of waitUntil: 'networkidle' in Playwright.


Using the Go SDK

If you prefer a typed interface, install the Go SDK:

go get github.com/snapsharp/sdk-go
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/snapsharp/sdk-go/snapsharp"
)

func main() {
	client := snapsharp.New("sk_live_YOUR_API_KEY")

	ctx := context.Background()

	img, err := client.Screenshot(ctx, &snapsharp.ScreenshotParams{
		URL:      "https://example.com",
		FullPage: true,
		Width:    1440,
		Format:   "png",
		DarkMode: true,
	})
	if err != nil {
		fmt.Fprintf(os.Stderr, "screenshot: %v\n", err)
		os.Exit(1)
	}

	if err := os.WriteFile("output.png", img, 0644); err != nil {
		fmt.Fprintf(os.Stderr, "write: %v\n", err)
		os.Exit(1)
	}
	fmt.Printf("done: %d bytes\n", len(img))
}

The SDK wraps the same HTTP API with Go-idiomatic types, handles error unmarshalling, and exposes all parameters as struct fields with documentation comments.


Advanced Use Cases

Automated PDF Reports

Generate PDF from any URL for scheduled reporting pipelines:

params := url.Values{}
params.Set("url", "https://your-dashboard.example.com/report")
params.Set("format", "pdf")
params.Set("full_page", "true")
// Requires growth plan or above

PDF rendering respects print media queries, so pages with @media print styles render correctly.

OG Image Generation for Dynamic Pages

Generate Open Graph images server-side and cache the result:

params := url.Values{}
params.Set("url", "https://your-app.example.com/og?title=My+Post&author=Jane")
params.Set("width", "1200")
params.Set("height", "630")
params.Set("format", "png")
params.Set("cache_ttl", "86400") // cache for 24h

See how to generate OG images in Next.js for the rendering side of this pattern.

Visual Monitoring in CI/CD

Compare screenshots across deployments to catch visual regressions:

// Capture before deployment
before, _ := takeScreenshot("https://staging.example.com", apiKey)
os.WriteFile("before.png", before, 0644)

// ... deploy ...

// Capture after deployment
after, _ := takeScreenshot("https://staging.example.com", apiKey)
os.WriteFile("after.png", after, 0644)

// Or use the /v1/diff endpoint to get a pixel-level diff image

The visual diff and monitoring article covers the /v1/diff endpoint in detail.

Batch Captures with Goroutines

The API is stateless, so concurrent requests are safe. Use a semaphore to stay within your plan's rate limit:

urls := []string{
	"https://example.com",
	"https://github.com",
	"https://go.dev",
}

sem := make(chan struct{}, 5) // max 5 concurrent requests
var wg sync.WaitGroup

for i, u := range urls {
	wg.Add(1)
	go func(idx int, target string) {
		defer wg.Done()
		sem <- struct{}{}
		defer func() { <-sem }()

		data, err := takeScreenshot(target, "sk_live_YOUR_API_KEY")
		if err != nil {
			fmt.Fprintf(os.Stderr, "failed %s: %v\n", target, err)
			return
		}
		os.WriteFile(fmt.Sprintf("shot-%d.png", idx), data, 0644)
	}(i, u)
}
wg.Wait()

Comparison: chromedp vs SnapSharp API

chromedp / rodSnapSharp API
Lines of setup code50+ (pool, context, flags)5 (HTTP request)
Docker image size+200–300 MB (Chrome binary)No change
RAM per capture~200 MB0 (cloud-side)
Concurrent capturesManual pool managementBuilt-in (your plan's rate limit)
Full-page screenshotsRequires scroll tricksfull_page=true
Dark modeemulatedMedia setupdark_mode=true
Ad blockingCustom request interceptionblock_ads=true
PDF exportPage.printToPDF + flagsformat=pdf
Chrome maintenanceYour responsibilityManaged
Works in Cloud Run / LambdaNo (binary too large)Yes
Free tierUnlimited (self-hosted cost)100 req/month

Getting Started

  1. Create a free account — 100 screenshots per month, no credit card required.
  2. Copy your API key from the dashboard.
  3. Replace sk_live_YOUR_API_KEY in any example above and run it.

Full parameter reference is in the screenshot API docs. The endpoint accepts url, width, height, format (png/jpeg/webp/pdf), full_page, dark_mode, device, wait_for, wait_until, block_ads, stealth, proxy, and more.

If your use case needs more than 100 screenshots per month, the Starter plan starts at 5,000 requests and adds full-page, WebP, and retina support.

Website Screenshots in Go: chromedp vs Screenshot API — SnapSharp Blog