Puppeteer Screenshot Hangs on networkidle0: What to Use Instead
If Puppeteer hangs at networkidle0, wait for the page content your screenshot needs. Compare lifecycle, selector, function, and network-idle waits with runnable examples.
If page.goto() hangs with waitUntil: 'networkidle0', stop making the screenshot wait for every network request to stop unless network quiet is part of the requirement. Navigate to a suitable document milestone, then wait for the element or application state that makes the image useful. Puppeteer’s screenshot guide demonstrates networkidle2, but it is an example, not a universal fix. Puppeteer screenshot guide
Why networkidle0 can stall
Navigation readiness and application readiness are different things. A network-idle condition waits for network activity to remain below a threshold for an interval. If activity continues, that condition may not be met even though the content you want has rendered. Raising the timeout can simply make the same mismatch take longer to fail.
This is a general explanation of the documented wait behavior, not a claim that every affected site uses a particular request type. Puppeteer’s navigation API lets you choose a lifecycle condition; its screenshot example uses networkidle2.
Choose the readiness condition your image needs
| Wait | Use it when | Limit |
|---|---|---|
domcontentloaded or load |
You need a document lifecycle milestone before checking the page. | Neither guarantees that app data or a specific component is ready. |
waitForSelector() or a locator wait |
A particular element must exist or be visible. | The selector and state must represent the content you need in the screenshot. |
waitForFunction() |
Readiness is expressed as a page-specific condition, such as a completed status. | The predicate must describe a state that eventually becomes true. |
networkidle2 or waitForNetworkIdle() |
A period of reduced network activity matters to your capture. | Traffic can keep the wait open; network quiet does not prove that the rendered content is correct. |
Puppeteer’s docs describe locator waits and recommend locators for element interactions. Check the docs for your installed release when choosing locator methods and options. Page interactions
Runnable Puppeteer patterns
Install Puppeteer in a Node.js project with npm install puppeteer. Save one of the following examples as an ES module file, such as capture.mjs, and run it with node capture.mjs. Replace the example URL and selector with the page and content relevant to your capture.
Wait for a meaningful visible element
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-testid="report"]', { visible: true });
await page.screenshot({ path: 'report.png', fullPage: true });
} finally {
await browser.close();
}
Choose a selector tied to the actual content needed in the image. If the element appears before its data is ready, wait for a stronger signal, such as a result count or completion indicator.
Wait for an application-specific state
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => {
const status = document.querySelector('[data-testid="report-status"]');
return status?.textContent?.trim() === 'Complete';
});
await page.screenshot({ path: 'report.png', fullPage: true });
} finally {
await browser.close();
}
Use this when the page shell exists before the result is complete. The callback runs in the page context. Make the predicate specific enough to avoid capturing a loading state.
Use a bounded network-idle wait when it is needed
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForNetworkIdle({ idleTime: 1000, timeout: 10000 });
await page.screenshot({ path: 'page.png' });
} finally {
await browser.close();
}
waitForNetworkIdle() remains a network condition. Puppeteer documents that it waits at least the configured idle time. Confirm the option names and timeout behavior against the version installed in your project. waitForNetworkIdle API
Capture one element
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const report = await page.waitForSelector('[data-testid="report"]', { visible: true });
if (!report) throw new Error('Report element was not found');
await report.screenshot({ path: 'report.png' });
} finally {
await browser.close();
}
Puppeteer’s screenshot guide documents element screenshots. Its element screenshot method scrolls the element into view by default when needed. Screenshot guide
Diagnose which operation is waiting
A report that “the screenshot hangs” can refer to navigation, a readiness wait, or the screenshot call itself. Log around each awaited operation so the timeout points to the actual step.
console.log('navigation: start');
await page.goto(url, { waitUntil: 'domcontentloaded' });
console.log('navigation: done');
console.log('readiness: start');
await page.waitForSelector('[data-testid="report"]', { visible: true });
console.log('readiness: done');
console.log('screenshot: start');
await page.screenshot({ path: 'report.png' });
console.log('screenshot: done');
- Identify the last log line. It distinguishes a stalled navigation from a stalled condition wait or capture.
- Use
domcontentloadedorloadwhere appropriate, then add the smallest meaningful readiness condition. - If network quiet is genuinely required, use a finite timeout and handle its failure as a deliberate diagnostic or fallback.
- For a component image, wait for that component and use its element screenshot method.
- Verify that the condition matches the output requirement: a visible shell may not mean the data is ready, and a quiet network does not establish correct content.
Or skip the browser setup
ScreenshotNeo provides a screenshot API: one GET request takes a URL and returns an image or PDF. See the ScreenshotNeo API documentation.
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(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Learn about ScreenshotNeo, then sign up for 1,000 free screenshots a month, no card required.
Performance, reliability, and cost
- Performance: A targeted selector or application condition can let the capture proceed once the required content is ready, without waiting for unrelated network activity. The actual time depends on the page and the condition chosen; no universal speed improvement can be promised.
- Reliability: Set finite timeouts for waits that may never resolve. A timeout should produce useful diagnostics or an explicit fallback, not silently create an image of an unknown state. Check the installed Puppeteer version for supported options.
- Capture correctness: Define readiness based on what must appear in the image. If lazy content or below-the-fold elements matter, make sure the capture flow causes that content to load before taking the screenshot.
- Cost: Browser automation consumes the runtime and infrastructure you operate. The supplied Puppeteer documentation does not establish a universal cost or benchmark. ScreenshotNeo bills only clean shots; bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses report page verdict and billing headers.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
page.goto() times out with networkidle0 |
The selected network-idle condition is not being reached. | Navigate with domcontentloaded or load, then wait for the content-specific condition. |
networkidle2 also times out |
Reduced network activity is still not reached for the required interval. | Treat it as an example, not a guarantee. Prefer a selector or app-state wait unless network quiet is required. |
| Selector wait times out | The selector is wrong, the element never appears, or the page has not reached the state assumed by the script. | Check the selector and page state; wait for the correct visible element or revise the readiness condition. |
| Screenshot contains a loading shell | The wait only established that the shell or container exists. | Wait for a completion marker, result value, or other application-specific signal. |
| Network-idle wait times out after navigation succeeds | The later wait is waiting for network quiet, independently of navigation. | Log before and after each await; bound the network wait and use a deliberate fallback if appropriate. |
| Element screenshot is empty or fails | The target may not be found, visible, or in the expected state. | Wait for the element to be visible and confirm it contains the content to capture. |
FAQ
Should I always replace networkidle0 with networkidle2?
No. Puppeteer’s screenshot guide demonstrates networkidle2, but it is still a network-idle condition and is not guaranteed to fit every page. Prefer the wait that matches the content requirement.
Does DOMContentLoaded mean the page is ready for a screenshot?
It means a document lifecycle milestone has occurred. It does not by itself establish that application data or a particular component is ready.
When is network idle the right choice?
Use it when reduced network activity itself matters to the capture. If your requirement is that a particular result is visible, wait for that result instead.
Where can I check exact wait options?
Consult the Puppeteer API documentation for the version installed in your project; option names and defaults can vary by release.


