Chrome DevTools Protocol Screenshots Are Blank: Causes and Fixes
Diagnose blank CDP screenshots by checking image data, clip geometry, rendering readiness, capture surfaces, and headless differences.

A blank Chrome DevTools Protocol (CDP) screenshot has two different meanings: Chrome may have returned no image data, or it may have returned a valid image whose pixels are empty, transparent, or outside the content you intended to capture. Start by separating those cases. Check the response for a data field, decode it as an image, then verify clip geometry, capture surface, viewport, and rendering readiness. The experimental HeadlessExperimental.beginFrame path can explicitly fail during renderer initialization; ordinary Page.captureScreenshot has different failure modes.
1. What a successful CDP screenshot contains
Page.captureScreenshot returns a base64-encoded image in the data result field. The method supports PNG, JPEG, and WebP, optional clipping, capture beyond the viewport, surface capture, and a speed-oriented optimization flag. See the official Page domain reference for current parameter definitions and defaults.

const result = await client.send('Page.captureScreenshot', {format: 'png', fromSurface: true, captureBeyondViewport: false, optimizeForSpeed: false});
if (!result || typeof result.data !== 'string' || result.data.length === 0) throw new Error('CDP returned no screenshot data');
const bytes = Buffer.from(result.data, 'base64');
require('fs').writeFileSync('shot.png', bytes);
console.log(`wrote ${bytes.length} bytes`);
A missing data value is a protocol or renderer problem. A present value that decodes to a white, transparent, or uniformly colored bitmap is a rendering, geometry, or styling problem. Chromium’s protocol browser tests distinguish these cases by checking protocol errors, the presence of data, decoded bitmap content, and expected colors (test source).
2. Minimal reproducible capture
Use a fresh page and a known document before changing options. This removes application-specific loading and CSS from the first diagnosis.
import { launch } from 'puppeteer';
import fs from 'node:fs/promises';
const browser = await launch({headless: true});
const page = await browser.newPage();
await page.setViewport({width: 1280, height: 800, deviceScaleFactor: 1});
await page.setContent(`<!doctype html><style>body{margin:0;background:#2463eb;color:white;font:32px sans-serif;padding:40px}</style>CDP smoke test`, {waitUntil: 'load'});
const session = await page.target().createCDPSession();
const reply = await session.send('Page.captureScreenshot', {format: 'png', fromSurface: true, captureBeyondViewport: false});
if (!reply.data) throw new Error('No data field in Page.captureScreenshot reply');
await fs.writeFile('smoke.png', Buffer.from(reply.data, 'base64'));
await browser.close();
If this produces a visible blue page, the protocol connection and renderer are functioning; focus on target readiness and capture parameters. If it fails, record the Chrome/Chromium version, launch flags, operating mode, and complete protocol error before changing code.
3. Check the requested region and viewport
A clip is measured in CSS pixels and can include x, y, width, height, and scale. A zero-sized clip, misplaced coordinates, or a region that does not intersect painted content can look blank even though the page is fine.
const metrics = await client.send('Page.getLayoutMetrics');
console.log(metrics.cssLayoutViewport, metrics.cssContentSize);
const clip = {x: 0, y: 0, width: Math.min(metrics.cssContentSize.width, 1280), height: Math.min(metrics.cssContentSize.height, 2000), scale: 1};
const shot = await client.send('Page.captureScreenshot', {format: 'png', clip, captureBeyondViewport: true, fromSurface: true});
captureBeyondViewport defaults to false. If the area is below the visible viewport, explicitly enable it and verify output dimensions. Treat this as a diagnostic check, not a universal fix: a wrong clip can create a blank image, but it is not the only cause. A CSS viewport of 1280×800 at device scale factor 2 can produce a 2560×1600 bitmap, so compare decoded dimensions with the CSS values you logged.
4. Confirm that the intended frame has painted
Navigation completion does not guarantee that a single-page application has rendered its data. Wait for the application’s own readiness signal, then allow a frame to be presented. Prefer a stable selector over an arbitrary sleep.
await page.goto('https://example.com/dashboard', {waitUntil: 'domcontentloaded'});
await page.waitForSelector('[data-page-ready="true"]', {timeout: 30000});
await page.evaluate(() => new Promise(requestAnimationFrame));
const image = await page.screenshot({type: 'png'});
When no readiness selector exists, combine a bounded network-idle wait with a frame check, and log which condition completed. Long-running analytics, WebSockets, or polling can prevent a permanent network-idle state, so a finite timeout plus a selector is more reliable than an unbounded wait.
For experimental HeadlessExperimental.beginFrame, the target must be created with BeginFrameControl. Its reference documentation warns that screenshot capture can fail during renderer initialization and no screenshot data is returned (HeadlessExperimental reference). Do not treat beginFrame as a drop-in replacement for Page.captureScreenshot; it is a separate route with explicit frame-driving requirements.
5. Surface, view, headless, and headful differences
The Page method’s fromSurface option defaults to true. If you experiment with surface versus view capture, change one setting at a time and compare decoded pixels. A compositor surface includes the final rendered surface; a view capture can behave differently for overlays or embedded surfaces. The documentation does not define one setting that fixes every blank result.
Headless and headful Chrome report different screen configurations: headless exposes a virtual headless screen, while headful reports the physical screen configuration (Emulation reference). Log viewport width and height, device scale factor, screen dimensions, and headless mode whenever output differs.
6. Systematic diagnostic checklist
- Capture the raw reply. Log method, parameters, protocol error, and whether
dataexists. Never silently replace a missing value with an empty file. - Validate the image. Decode base64 and inspect magic bytes (PNG starts with
89504e47; JPEG withffd8ff). Record byte length and bitmap dimensions. - Remove optional parameters. Retry with only
format: 'png'. Add clip, scale, beyond-viewport, surface, and optimization flags one at a time. - Use a known page. Capture static HTML or a data URL. If that works, investigate the target site’s scripts, permissions, or bot checks.
- Check readiness. Wait for a meaningful selector and one animation frame. Record navigation and selector timeouts separately.
- Compare modes and sizes. Reproduce at a smaller viewport and output. Keep Chrome version and launch flags in logs. A Chromium issue listing mentions large-screenshot problems, but the available report does not establish affected versions or a verified workaround (issue listing).
7. Common blank-image symptoms and fixes
| Symptom | Likely evidence | Action |
|---|---|---|
No data field |
Protocol error, renderer startup, or begin-frame initialization | Inspect the full error; verify target readiness and BeginFrameControl; retry the ordinary Page method. |
| Valid image, all white | CSS background or app content has not painted | Wait for the ready selector, check console errors, and capture after a frame. |
| Valid image, fully transparent | Transparent page/background or empty clip | Set an explicit test background, remove the clip, and compare fromSurface values. |
| Only a corner or empty strip | Clip coordinates, scale, or viewport mismatch | Log layout metrics, use a viewport-sized clip, then enable beyond-viewport when needed. |
| Works headful, blank headless | Different virtual screen, flags, or timing | Log emulation and screen metrics; set viewport/device scale explicitly; wait on a selector. |
| Large captures fail or truncate | Size-sensitive browser/build behavior | Reduce dimensions, split the page, and compare a current supported build; do not assume an issue listing proves a universal fix. |
8. Reliability and performance practices
- Bound every wait. Use explicit navigation, selector, and screenshot timeouts. On timeout, save HTML, console messages, and a small diagnostic screenshot if possible.
- Reuse the browser carefully. Keep one browser process and create isolated pages or contexts per job. Close pages after capture to limit memory growth from large bitmaps.
- Choose output deliberately. PNG preserves text and transparency; JPEG is smaller for photographic pages; WebP can reduce transfer size when supported.
- Control dimensions. Very tall pages increase encode time and memory. Capture bounded clips when a full page is unnecessary; for long documents, capture sections and stitch them.
- Make failures observable. Store Chrome version, URL, viewport, device scale, clip, format, duration, response byte count, and whether data was present.
- Retry selectively. Retry transient navigation or target-detachment errors with a fresh page. Do not retry a deterministic zero-sized clip or missing readiness selector.
9. Cost and operational trade-offs
Running CDP yourself means paying for browser CPU, memory, storage, and engineering time. Large screenshots consume more memory during rasterization and base64 encoding. A queue with concurrency limits protects the host, while caching identical URL and option combinations avoids duplicate work. Keep credentials and authenticated cookies out of logs.
10. Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. It accepts one GET request and returns PNG, JPEG, WebP, or PDF. Cookie and consent banners are accepted before capture, then 60+ known consent platforms, newsletter popups, and chat widgets are removed; each step can be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses report the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for all options.
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}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
It supports full-page capture with lazy images loaded, element selectors, dark mode, device presets or custom viewports, retina scale, custom CSS and JavaScript, clicks, waits, blocking rules, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed links, async jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI spec. Parameter names used by other screenshot APIs also work, simplifying migration. Plans include 1,000 free shots each month with no card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
11. FAQ
Does a blank PNG prove Chrome failed?
No. A valid PNG filled with white or transparent pixels proves bytes were returned; inspect the page, clip, background, and readiness. A missing data field is stronger evidence of protocol or renderer failure.
Should I always set captureBeyondViewport to true?
No. The documented default is false. Enable it when your requested region is outside the viewport, and verify clip coordinates and output dimensions first.
Is HeadlessExperimental.beginFrame the fix?
It is a separate experimental route that requires BeginFrameControl and can return no screenshot during renderer initialization. Use it only when you need explicit frame control and can satisfy those target requirements.
Why does the same URL differ in headless and headful mode?
The modes expose different screen configurations and often use different timing and flags. Log viewport, screen, device scale, and readiness conditions before comparing pixels.


