Why Does Puppeteer Return a Blank Screenshot of My Website?
A blank screenshot can mean the page never rendered, the app was not ready, or the capture missed its content. Diagnose each case with Puppeteer checks and runnable code.
A blank Puppeteer screenshot does not point to one universal bug. First confirm that navigation reached the intended URL and inspect the HTTP status. Then check whether the expected content rendered, wait for a meaningful app-specific readiness condition, and inspect the screenshot’s viewport, clipping, background, and output options.
A resolved page.goto() call does not prove that the intended site returned a healthy page: navigation to about:blank can resolve with no response, and in headless shell a 404 or 500 response does not itself make navigation throw. Puppeteer’s docs describe the APIs and relevant environment issues, but do not identify a single cause for every blank screenshot. Puppeteer Page.goto()
1. Diagnose the blank screenshot in order
- Confirm navigation. Log the requested URL, final
page.url(), and response status. Check redirects and whether the response is missing or an error. - Check what rendered. Inspect the document title, body text, and a selector that should exist on the page. An empty DOM or error page points toward navigation or application behavior.
- Wait for the app’s content. Wait for a meaningful selector or application readiness condition. Navigation completion alone may happen before client-rendered content is ready.
- Check capture boundaries. Verify viewport size,
clip,fullPage, andcaptureBeyondViewport. - Check image settings and file. Confirm the output path, extension or explicit image type, and whether transparent background was requested.
- Inspect the runtime if the page is empty or Chrome is unhealthy. Capture browser and page errors; in containers, check browser dependencies and writable profile directories.
2. A runnable Puppeteer diagnostic script
This script opens a URL, reports the final URL and response status, logs browser-side errors and failed requests, waits for an expected visible selector, and saves a full-page PNG. Replace the URL and selector with values from your site.
import puppeteer from 'puppeteer';
const url = process.argv[2] ?? 'https://example.com';
const expectedSelector = process.env.EXPECTED_SELECTOR ?? 'body';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
page.on('console', message => {
if (message.type() === 'error') console.error('PAGE CONSOLE:', message.text());
});
page.on('pageerror', error => console.error('PAGE ERROR:', error.message));
page.on('requestfailed', request => {
console.error('REQUEST FAILED:', request.url(), request.failure()?.errorText);
});
let response;
try {
response = await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 45_000,
});
} catch (error) {
console.error('NAVIGATION ERROR:', error.message);
throw error;
}
console.log({
requestedUrl: url,
finalUrl: page.url(),
status: response?.status() ?? null,
title: await page.title(),
});
const bodyText = await page.locator('body').innerText().catch(() => '');
console.log('BODY TEXT PREVIEW:', bodyText.slice(0, 500));
if (expectedSelector !== 'body') {
await page.waitForSelector(expectedSelector, {
visible: true,
timeout: 20_000,
});
}
await page.screenshot({
path: 'page.png',
type: 'png',
fullPage: true,
omitBackground: false,
});
console.log('Saved page.png');
} finally {
await browser.close();
}
Run it with node diagnose.mjs https://your-site.example. To wait for a specific element, set EXPECTED_SELECTOR='#main-content' node diagnose.mjs https://your-site.example in a POSIX shell, or set that environment variable using your platform’s normal syntax. This is a diagnostic pattern, not a guaranteed fix: a selector may appear before images or other visual work is finished.
3. Choose a readiness condition that fits the site
The navigation lifecycle and the application’s own readiness are different questions. Puppeteer’s screenshot guide demonstrates networkidle2, while page.waitForSelector() can wait for a chosen DOM element. Neither is universally right for every website. Puppeteer screenshots guide · Page.waitForSelector()
| Condition | Useful when | What it does not prove |
|---|---|---|
domcontentloaded |
You want the initial document parsed, then will wait for app-specific content. | It does not guarantee client rendering, images, or asynchronous data are finished. |
load |
The page’s load event is an appropriate initial boundary. | It does not guarantee that a single-page app has completed later work. |
networkidle2 |
The site settles its network activity and this is a useful readiness signal. | Long polling or persistent connections may prevent it from being useful; network quiet does not prove visual completeness. |
waitForSelector(selector, { visible: true }) |
A known visible element indicates the content you need has appeared. | The selector may appear before images, fonts, or other content are ready. |
| App-specific readiness | Your application exposes a state or element that means its key content is ready. | It only guarantees what the application’s readiness signal is designed to mean. |
For example, if an app displays its main content after an API call, wait for the main content rather than adding an arbitrary long sleep. If the selector appears before an image is decoded, wait for that image’s own condition as well.
4. Check viewport, clipping, and screenshot options
A screenshot can be valid but capture the wrong region. Puppeteer’s ScreenshotOptions documentation lists these defaults and options:
| Option | What to verify |
|---|---|
fullPage |
Defaults to false. Set to true to capture the full page rather than only the viewport. |
clip |
An explicit rectangle can exclude the content. Check its coordinates and dimensions. |
captureBeyondViewport |
Defaults to false when no clip is supplied and true when a clip is supplied. Review it when capturing beyond the viewport. |
omitBackground |
Defaults to false. When true, the default white background is hidden and the result can be transparent. The page itself may still be rendered. |
path |
The file is saved at this path; relative paths resolve from the current working directory. Without a path the screenshot is not written to disk. |
type and quality |
Type defaults to PNG. Quality applies to JPEG or WebP, not PNG. Check extension and chosen format together. |
encoding |
Defaults to binary. Base64 is available when you need encoded data rather than binary image bytes. |
fromSurface |
Defaults to true; it controls capture from the surface rather than the view. |
optimizeForSpeed |
Defaults to false; it is a speed-oriented capture option. |
Set a known viewport before navigation or capture so that responsive layouts behave predictably. When expected content is below the fold, compare a viewport screenshot with fullPage: true. When using clip, temporarily remove it to determine whether the region is the problem.
5. Find out whether the page or browser is failing
If the final URL, status, title, and body text do not match expectations, troubleshoot navigation or the site before changing screenshot settings. A status of 404 or 500 can still be a resolved navigation response in headless shell; inspect response.status() rather than treating a resolved promise as a successful page. A null response can occur for about:blank or same-URL hash navigation. Page.goto() behavior
If the page data is present but the screenshot is empty, check the capture region and output options. If the browser logs page errors or request failures, investigate those messages and the corresponding application resources. Puppeteer’s troubleshooting guide also notes that Chrome needs compatible system dependencies in environments such as Alpine and a writable user-data directory; these are environment checks, not an assumed explanation for an individual blank image. Puppeteer troubleshooting
6. Common errors and their fixes
| Symptom | Likely branch to inspect | Next step |
|---|---|---|
| Navigation resolves, but the page is blank or unexpected | Wrong URL, redirect, error response, or content has not rendered. | Log page.url(), response status, title, and body text; wait for an expected page element. |
response is null |
Navigation to about:blank or a same-URL hash change. |
Confirm the requested URL and the final page.url(). |
| Navigation resolves with 404 or 500 | The server returned an HTTP error status. | Inspect response.status() and the page content; a resolved navigation is not evidence that the response was successful. |
| Screenshot shows only part of the page | Viewport-only capture or a restrictive clip. | Try fullPage: true, remove clip, and check viewport dimensions. |
| Selector wait times out | The selector is wrong, absent, or the app did not reach the expected state. | Inspect the DOM and app errors; confirm the selector and use the state that actually signals readiness. |
networkidle2 never completes |
The page may keep network activity open. | Use a more appropriate navigation condition and wait for a page-specific selector or readiness signal instead. |
| Chrome fails to launch in a container | Missing compatible dependencies, an unavailable browser executable, or profile permissions. | Follow the Puppeteer troubleshooting guidance for the host; ensure the browser is installed and its user-data directory is writable. |
| Chrome reports a sandbox error | Host sandbox configuration or permissions. | Follow Puppeteer’s environment-specific sandbox guidance. The docs strongly discourage running Chrome without a sandbox. |
| File appears missing or empty | Wrong working directory/path, no path, or code failed before saving. |
Use an explicit absolute output path while diagnosing and confirm the screenshot call completed. |
| Image looks transparent or white | omitBackground or page styling. |
Set omitBackground: false and inspect the page’s background styles. |
7. Performance, reliability, and cost considerations
- Wait for the condition you need. A fixed delay can waste time and still miss late content. A site-specific selector or readiness signal usually makes the condition explicit.
- Use network idle selectively. It can be useful for pages that settle, but persistent connections can make it a poor fit. Pair it with a timeout and a plan for site-specific readiness.
- Keep diagnostic output targeted. Log the final URL, status, relevant console errors, failed requests, and a short body-text preview. This gives useful evidence without dumping an entire page.
- Make capture dimensions deliberate. A full-page image can be larger than a viewport image. Choose full-page capture only when the whole document is needed.
- Make failures observable. Distinguish navigation errors, HTTP error responses, readiness timeouts, and screenshot-save errors in logs so retries target the right step.
- Account for browser runtime costs. A self-hosted Puppeteer job uses your own browser runtime and infrastructure. Its execution time depends on site loading, readiness waits, and capture size; the supplied Puppeteer docs do not provide a universal cost or timing benchmark.
8. Or skip the browser setup
If your goal is a clean image rather than debugging a local Chromium run, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
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)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
See the ScreenshotNeo API documentation for request options. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
9. FAQ
Does a successful page.screenshot() call mean the website rendered correctly?
No. It means Puppeteer produced a capture according to the supplied options. Check the page content and capture area separately.
Should I always use networkidle2?
No. It is one possible readiness condition. Choose a condition that matches the page, and use an app-specific selector when it better represents the content you need.
Is fullPage: true the right fix for every blank image?
No. It helps when the expected content is outside the viewport. If the document itself is empty or the wrong page loaded, fix that branch first.
Does Puppeteer document a universal blank-screenshot bug?
No. The relevant documentation explains navigation, readiness, capture options, and runtime troubleshooting, but does not establish one cause for every blank screenshot.


