Fix Go chromedp Screenshots That Return an Empty Image
Diagnose blank, empty, clipped, or incomplete chromedp screenshots by checking capture mode, page readiness, element bounds, image dimensions, and Chrome setup.
A chromedp screenshot can look “empty” for several different reasons: the Go byte slice may be empty, the image may decode but show a white page, the capture may be clipped, or it may show the wrong region. Start by recording which outcome you have and whether chromedp.Run returned an error. Then check that you chose the right screenshot action, waited for the content you need, and used reasonable dimensions.
The examples below use the current chromedp screenshot actions. Issue reports are useful clues, but they describe specific older versions and environments; they do not establish a universal Chrome limit or a fix for every blank capture. See the [chromedp screenshot implementation](https://github.com/chromedp/chromedp/blob/main/screenshot.go) and the [project README](https://github.com/chromedp/chromedp).
1. Identify what “empty” means
Before changing flags or adding sleeps, capture four facts: the error from Run, the byte count, whether the bytes decode as an image, and what the image actually shows. A non-empty PNG of a white page is not the same failure as an empty buffer or a screenshot of the wrong part of the page.
var imageBytes []byte
err := chromedp.Run(ctx, chromedp.Navigate(targetURL), chromedp.CaptureScreenshot(&imageBytes))
if err != nil {
log.Printf("capture error: %v", err)
}
log.Printf("screenshot bytes: %d", len(imageBytes))
if len(imageBytes) > 0 {
decoded, format, decodeErr := image.Decode(bytes.NewReader(imageBytes))
log.Printf("decode format=%q size=%v error=%v", format, decoded.Bounds(), decodeErr)
}
Imports for this diagnostic snippet are bytes, image, and log. Register PNG decoding with a blank import of image/png; add image/jpeg if you are also decoding JPEG output.
| Observed result | Likely investigation |
|---|---|
Run errors |
Read the exact error first. Navigation, timeout, selector, protocol, and capture-size failures need different fixes. |
| Zero bytes and no error | Check that the action ran, its output pointer is the one you inspect, and no later task overwrote or reset it. |
| Bytes do not decode | Check whether the response is truncated or whether you saved a different buffer than the capture populated. |
| Valid image is white/blank | Check navigation, app rendering, browser mode, and readiness conditions. |
| Valid image is clipped or wrong | Check action choice, viewport/capture dimensions, selector, scroll position, and element geometry. |
| Images or widgets are missing | Wait for the specific resources or app state required in the screenshot. |
2. Match the screenshot action to the output you want
chromedp provides separate actions for a selected element, the current viewport, and a capture beyond the viewport. They are not interchangeable.
| Action | Use it for | Important detail |
|---|---|---|
chromedp.Screenshot(selector, &buf, ...) |
The first visible element matching a selector. | It is an element query action. A missing matching node returns an error. The current source derives a clip from matching nodes’ client rectangles. |
chromedp.CaptureScreenshot(&buf) |
The current browser viewport. | It does not mean the entire document. |
chromedp.FullScreenshot(&buf, quality) |
A capture extending beyond the viewport. | Quality 100 selects PNG; other quality values select JPEG, according to the implementation comments. |
These behaviors are documented in [chromedp’s screenshot source](https://github.com/chromedp/chromedp/blob/main/screenshot.go). If you need an element screenshot, first verify the selector identifies the intended element and that it is visible. If you need only the visible screen, use the viewport action rather than assuming the element or full-page action means the same thing.
3. Runnable Go example: wait for a page, then capture
This complete program captures a full page after navigation and a page-specific readiness check. Replace the URL, selector, and ready condition with values for your page. The selector wait proves that the target element is visible; it does not prove that every image or asynchronous component inside it has finished loading.
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/chromedp/chromedp"
)
func main() {
const targetURL = "https://example.com"
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 45*time.Second)
defer cancel()
var png []byte
err := chromedp.Run(ctx,
chromedp.Navigate(targetURL),
chromedp.WaitVisible("body", chromedp.ByQuery),
// Replace this condition with a marker your app sets when rendering is done.
chromedp.Evaluate(`document.readyState === "complete"`, nil),
chromedp.FullScreenshot(&png, 100),
)
if err != nil {
log.Fatalf("capture %s: %v", targetURL, err)
}
if len(png) == 0 {
log.Fatal("capture returned zero bytes")
}
if err := os.WriteFile("page.png", png, 0o644); err != nil {
log.Fatal(err)
}
fmt.Printf("wrote page.png (%d bytes)\n", len(png))
}
Install the dependency with go get github.com/chromedp/chromedp, then run go run .. If your app hydrates after document.readyState becomes complete, replace that generic check with a selector or JavaScript condition tied to the app’s finished state. The official [chromedp screenshot example](https://github.com/chromedp/examples/blob/master/screenshot/main.go) also shows element and full-page capture patterns.
4. Wait for the content that must appear
A visible target can still contain an image that has not loaded or a component that is waiting on client-side data. A report using chromedp v0.7.6 and Chrome 88 described a slow image missing because capture happened while the page was loading. That report is a timing example, not proof that every incomplete screenshot has the same cause: [slow-image report](https://github.com/chromedp/chromedp/issues/954).
Prefer an explicit condition over an arbitrary sleep. For a page with a known image, wait for its load state and natural dimensions:
err := chromedp.Run(ctx,
chromedp.Navigate(targetURL),
chromedp.WaitVisible("#hero", chromedp.ByQuery),
chromedp.Evaluate(`new Promise(resolve => {
const img = document.querySelector("#hero img");
if (!img) { resolve(false); return; }
const finish = () => resolve(img.complete && img.naturalWidth > 0);
if (img.complete) finish();
else {
img.addEventListener("load", finish, { once: true });
img.addEventListener("error", () => resolve(false), { once: true });
}
})`, nil),
chromedp.Screenshot("#hero", &png, chromedp.ByQuery),
)
For production code, evaluate and check a boolean result (rather than passing nil) and return a useful error if the image did not load. If the page has many relevant images, evaluate a condition across the images that matter. Avoid waiting for every image on pages with intentionally lazy or below-the-fold content unless the capture requires them.
Readiness checks that fit the page
- Wait for a page-owned marker such as
[data-render-complete="true"]after the application finishes rendering. - For a specific image, check
completeandnaturalWidth > 0; an image can be complete after a failed request, so check natural dimensions too. - For lazy-loaded content, scroll the target into view before waiting for its image, or use a capture strategy that causes the relevant region to render.
- Use a bounded context timeout. A condition that never becomes true should produce a diagnosable timeout, not a hung worker.
5. Check selector geometry and scroll position
For element captures, log the selector, matched element, bounding rectangle, and scroll position immediately before the screenshot. The current implementation calculates a client rectangle and rounds clip dimensions. An older report described capturing the wrong region after scrolling in chromedp v0.7.3 and Chrome 91; treat it as a reason to reproduce with your versions, not as evidence that current releases always have that defect: [scrolled element report](https://github.com/chromedp/chromedp/issues/844).
var geometry map[string]any
err := chromedp.Run(ctx,
chromedp.Evaluate(`(() => {
const el = document.querySelector("#target");
if (!el) return { found: false, scrollX, scrollY };
const r = el.getBoundingClientRect();
return { found: true, x: r.x, y: r.y, width: r.width,
height: r.height, scrollX, scrollY, visible: r.width > 0 && r.height > 0 };
})()`, &geometry),
)
if err != nil { log.Fatal(err) }
log.Printf("target geometry: %#v", geometry)
If the rectangle has zero width or height, inspect CSS visibility, whether the selector matched the intended node, and whether the page has rendered. If the element is offscreen or sticky, compare the capture before and after scrolling the target into view. If the selector matches multiple nodes, Screenshot documents behavior around the first matching node; make the selector specific.
6. Reduce unusually large capture dimensions
Retry with a normal viewport and a smaller capture area if a very large screenshot is blank, clipped, or returns a protocol error. Historical reports include a 7086 × 9448 emulated viewport associated with blank or cut-off output and a separate 2880 × 20544 capture error in one environment. The latter report mentioned a 16384 maximum texture dimension for that environment. These are diagnostic observations, not universal browser limits: [large viewport report](https://github.com/chromedp/chromedp/issues/1122) and [capture-size error report](https://github.com/chromedp/chromedp/issues/1215).
- Remove custom
EmulateViewportsettings temporarily. - Capture the current viewport. If it works but the full capture fails, investigate full-document dimensions and memory pressure.
- Try a smaller element or region, then increase the area gradually.
- Record the exact width, height, device scale, action, Chrome build, and error for each attempt.
Do not infer a general maximum from one report. For long pages, consider whether the whole page is actually needed; a viewport or target-element capture is cheaper to render and store. Do not assume chromedp automatically tiles a long page into multiple captures.
7. Compare headless and headed browser setup
chromedp runs Chrome headless by default, as stated in its [README](https://github.com/chromedp/chromedp). If possible, compare a small reproducible capture in headless and headed mode while keeping the browser build, page, dimensions, and flags fixed. Record the Chrome executable and version, OS/container, chromedp version, and flags. A 2024 issue reports a white page in one headless configuration that included DisableGPU; it does not establish that toggling that flag is a general fix: [headless blank-page report](https://github.com/chromedp/chromedp/issues/1510).
Do not blindly add or remove GPU flags. Change one flag at a time and compare results. Also check browser startup logs, whether the page navigated away from about:blank, and whether the browser process can access required fonts, certificates, and network resources. The chromedp README identifies the chromedp/headless-shell image as a headless environment option.
8. Troubleshooting checklist
| Symptom/error | Likely cause | Next step |
|---|---|---|
| Selector query returns no nodes | Wrong selector, content not rendered, or frame/context mismatch. | Log the selector and query result; wait for a page-specific marker; verify the intended frame. |
| Element screenshot is blank or tiny | Element has zero dimensions, is hidden, or is not the target node. | Log its bounding rectangle, visibility, and matched element count before capture. |
| Viewport shot is blank but navigation has no error | Capture occurred before application rendering, page stayed blank, or browser environment differs. | Check current URL/title and page marker; compare headed/headless with versions and flags recorded. |
| Image is missing but surrounding page appears | Image request is pending, failed, lazy, or its parent has not rendered. | Wait for the relevant image’s completion and nonzero natural dimensions; inspect its source and load error. |
| Wrong region after scrolling | Selector geometry, scroll state, or coordinate assumptions are wrong. | Log getBoundingClientRect() and scroll offsets; reproduce against current chromedp and Chrome. |
| “Unable to capture screenshot” or protocol error on a tall page | Capture extent or browser resource constraints may be involved. | Retry a viewport or smaller element capture; reduce dimensions and retain the exact error and versions. |
| Capture context deadline exceeded | Navigation, readiness condition, or capture exceeded the deadline. | Separate navigation and readiness timings; set a bounded timeout appropriate to the page and report which stage timed out. |
| Image decode fails | Buffer is empty, truncated, overwritten, or not the expected capture output. | Log length, check the action’s error, and write the same buffer immediately after capture. |
9. Performance, reliability, and cost
- Capture the smallest useful area. A viewport or element capture avoids producing and storing a very tall image when the full page is unnecessary.
- Wait narrowly. Waiting for a specific application marker or image avoids unnecessary delays from unrelated requests and below-the-fold resources.
- Use bounded contexts. Timeouts keep stalled navigation and readiness checks from occupying workers indefinitely; log the stage that timed out.
- Keep a reproducible record. Save the page URL, action, selector, dimensions, scroll state, readiness condition, versions, flags, error, and output shape. This makes intermittent failures comparable.
- Separate rendering from storage. Confirm the image decodes before treating a file-write or downstream upload issue as a Chrome capture problem.
Chrome screenshots consume browser memory and produce image data that grows with pixel area. Very tall or high-scale captures can be more demanding than viewport shots. The cited reports do not provide a general performance benchmark or universal maximum dimension, so size limits should be measured in the actual browser environment.
10. Or skip the browser setup
If your goal is a screenshot rather than debugging chromedp itself, [ScreenshotNeo](https://screenshotneo.com) is a website screenshot API: one GET request returns PNG, JPEG, WebP, or PDF. See the [ScreenshotNeo API documentation](https://screenshotneo.com/docs/) for parameters.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo accepts cookie and consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is on every plan. [Create a free ScreenshotNeo account](https://screenshotneo.com/account/sign-up/) to get 1,000 screenshots a month with no card.
11. Frequently asked questions
Does WaitVisible guarantee that the screenshot is complete?
No. It establishes that the selected element is visible. Images, client-side rendering, and other asynchronous content may still be unfinished.
Should I set screenshot quality to 100?
Use 100 when you want PNG from FullScreenshot. The chromedp implementation uses JPEG for other quality values. Changing format does not fix page readiness or geometry.
Is a white image proof that chromedp returned an empty buffer?
No. A white image may be a valid decoded image of a blank page. Check the byte length and decode result separately.
Is there a universal maximum full-page screenshot height?
The cited reports do not establish one. Limits depend on the browser and environment; reduce the capture area and measure with the exact setup you deploy.


