How to Capture a Web Page Screenshot After Network Requests Finish
Wait for network activity to go quiet before capturing a page, then add a page-specific readiness check when the content you need depends on JavaScript.
To capture a page after its initial network activity finishes, navigate with a network-idle condition and then take the screenshot. In Playwright, use waitUntil: 'networkidle'; in Puppeteer, the documented screenshot example uses waitUntil: 'networkidle2'. Network idle is a quiet-period heuristic, not proof that every image, animation, client-side render, or application task is finished. If a particular result matters, also wait for the element or state that shows it is ready.
This guide covers Node.js examples for Playwright and Puppeteer, plus cURL and Python options using ScreenshotNeo when you do not want to manage a browser. For official API details, see the Playwright Page API and Puppeteer screenshot guide.
1. Playwright: wait for network idle, then capture
Install Playwright and its Chromium browser in your project:
npm install playwright
npx playwright install chromium
Save this as screenshot.js and run it with node screenshot.js. It navigates to the target, waits for Playwright’s network-idle condition, saves a full-page PNG, and closes the browser even if navigation or capture fails.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
page.setDefaultNavigationTimeout(30_000);
page.setDefaultTimeout(15_000);
await page.goto('https://example.com', {
waitUntil: 'networkidle',
timeout: 30_000,
});
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
})();
Playwright defines networkidle as no network connections for at least 500 ms. Its documentation discourages treating that condition as a general readiness signal for testing. A page can be network-quiet while still hydrating or rendering, and some pages never become quiet because they poll or keep connections open.
Wait for the content you need
When the screenshot must include a specific result, wait for that result explicitly after navigation. Use a locator that represents the page state you care about:
await page.goto('https://example.com/results', { waitUntil: 'domcontentloaded' });
await page.getByRole('heading', { name: 'Search results' }).waitFor({ state: 'visible' });
await page.locator('[data-state="loaded"]').waitFor({ state: 'visible' });
await page.screenshot({ path: 'results.png', fullPage: true });
Choose a selector or accessible locator that appears only when the relevant content is ready. Avoid waiting for an arbitrary long delay if the page exposes a meaningful state. Locator waits have finite timeouts; if the state never appears, the wait fails instead of silently capturing an incomplete page.
2. Puppeteer: use networkidle2 or waitForNetworkIdle
Install Puppeteer, which downloads a compatible browser as part of installation:
npm install puppeteer
The official screenshot guide demonstrates navigation with waitUntil: 'networkidle2' before capturing. Save as screenshot-puppeteer.js and run node screenshot-puppeteer.js:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
page.setViewport({ width: 1440, height: 900 });
page.setDefaultNavigationTimeout(30_000);
await page.goto('https://example.com', {
waitUntil: 'networkidle2',
timeout: 30_000,
});
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
})();
Puppeteer also provides page.waitForNetworkIdle() when you want to navigate using a different lifecycle condition and then wait separately. Its options include an idle-time threshold and a concurrency threshold; consult the current API reference for the version installed in your project.
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForNetworkIdle({ idleTime: 500, timeout: 30_000 });
await page.screenshot({ path: 'page.png', fullPage: true });
Check the Puppeteer version’s API documentation before relying on an option name or default, because the documentation and library versions can change.
3. What “all network requests finished” means
Browsers do not have one universal signal that guarantees all work associated with a page is complete. A network-idle condition means network activity has stayed below a library’s threshold for a period. It does not certify that the page has finished every visual or application task.
- Polling and analytics: recurring requests can prevent the idle condition from happening or delay it unpredictably.
- Streaming connections: long-lived connections such as event streams or WebSockets may remain open even though the visible page is usable.
- Client-side rendering: scripts can update the page after the last request completes.
- Lazy images: images may not request data until they enter the viewport. A full-page screenshot can therefore need scrolling or a tool that triggers lazy loading.
- Animations and transitions: they can continue after the network has gone quiet, so the exact captured frame may vary.
- Third-party resources: ads, consent tools, or widgets can keep issuing requests independently of the main content.
For a reliable capture, use the least restrictive navigation condition that gets the document loaded, then wait for the application state you need. Use a network-idle wait only when the page’s request behavior makes it useful.
4. Screenshot options and browser choices
| Need | Approach |
|---|---|
| Simple capture after a quiet period | Playwright page.goto(..., { waitUntil: 'networkidle' }) or Puppeteer networkidle2. |
| Specific content must be visible | Wait for a locator or application state, then capture. This makes the readiness condition explicit. |
| Network idle after a different navigation milestone | In Puppeteer, navigate with a lifecycle condition such as domcontentloaded, then call page.waitForNetworkIdle(). |
| Another browser engine | Playwright’s documented workflow supports Chromium, Firefox, and WebKit. Select the engine that matches your target environment. |
| Low-level Chromium capture control | Chrome DevTools Protocol exposes Page.captureScreenshot, including format, clipping, and beyond-viewport options. Use it when direct Chromium protocol control is useful; standard automation APIs are simpler for ordinary captures. |
Playwright screenshot options include a file path, image type, full-page capture, and clipping. For example, await page.screenshot({ path: 'page.jpg', type: 'jpeg', quality: 80 }) saves a JPEG; fullPage: true captures beyond the viewport. See the Playwright screenshot API for supported options in your installed version. The Chrome DevTools Protocol reference documents lower-level capture parameters.
5. cURL, Python, and Node.js without browser setup
Browser automation is useful when you need direct control over browser state and readiness. If you want a screenshot from one API request instead, ScreenshotNeo accepts a URL and returns an image or PDF. See the ScreenshotNeo API documentation for its request parameters and response behavior. The API performs the capture for you; it does not expose a network-idle wait parameter in the facts provided here, so use browser automation when you specifically need to define readiness with a locator or network-idle condition.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as image_file:
image_file.write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));
These examples use the API endpoint and request shape documented by ScreenshotNeo. Supply your own API key and target URL. For available capture settings—including viewport, full-page capture, custom waits, and output options—check the docs rather than assuming browser-library option names apply to the API.
6. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Make one GET request with a URL to receive a screenshot or PDF:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo docs for API parameters. ScreenshotNeo accepts cookie and consent banners before capture and removes 60+ known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month—no card required.
7. Reliability, performance, and cost
Set bounded waits and clean up
Give navigation and explicit readiness waits finite timeouts. A page that polls forever or never renders its target state should produce a handled failure, not a worker that hangs indefinitely. Put browser closure in a finally block, as in the examples. In a service that captures many pages, also make sure each task releases its page and browser resources when it finishes or fails.
Choose the wait that matches the page
Waiting for network idle can add time when a site continuously makes requests. Waiting only for DOM content can be faster but may capture before client-rendered content appears. A visible application state is often a better balance when you know what the screenshot needs to show. There is no universal wait duration that makes every site ready.
Keep captures reproducible
- Use a fixed viewport and browser engine when comparing captures.
- Wait for a stable page-specific state before capture.
- Account for animations, lazy-loaded content, and third-party elements if they affect the image.
- Use finite timeouts and record whether failure occurred during navigation, readiness, or screenshot writing.
Self-hosted browser automation has browser installation, execution, and maintenance costs that depend on your environment and workload; the dossier provides no benchmark or universal cost figure. With ScreenshotNeo, the listed monthly tiers are Free: 1,000 shots, Starter: $5 for 3,000, Growth: $15 for 15,000, Pro: $39 for 60,000, Scale: $99 for 250,000, and Business: $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Check the product site for current plan details.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
networkidle times out |
The page keeps polling, streaming, or loading third-party requests. | Navigate with domcontentloaded or another suitable milestone and wait for the specific visible content. Increase the timeout only when the page genuinely needs more time. |
| Screenshot is blank or missing client content | The network became quiet before hydration or client-side rendering completed. | Wait for the content locator or application-ready state before calling screenshot(). |
| Some images are absent in a full-page capture | Lazy loading deferred image requests until scrolling or viewport entry. | Trigger the page’s lazy content by scrolling, or use a capture workflow that loads lazy images; then wait for the image elements you need. |
| Capture contains a loading spinner | The selector chosen for readiness appeared before the data-dependent view finished. | Wait for a state that represents completed data, such as the result list being populated or the loading indicator becoming hidden. |
| Capture varies between runs | Dynamic content, animation, timing, ads, or third-party widgets changed. | Use a stable readiness condition and control the viewport. If appropriate, disable animation or hide irrelevant elements with supported browser or capture options. |
| Browser process remains open after an error | Cleanup did not run after navigation or capture failed. | Close the browser in a finally block and handle task failures at the caller. |
| Screenshot file cannot be opened | The request failed or returned an error response that was saved as if it were an image. | Check the HTTP status before writing the body, verify the key and target URL, and consult the API response and docs. |
9. Frequently asked questions
Does taking a screenshot wait for requests automatically?
No. The screenshot call captures the page state when invoked. Add an explicit navigation, network-idle, or application-state wait first.
Is network idle the same as the page being fully loaded?
No. It indicates a quiet network period according to the library’s threshold. JavaScript rendering, animations, and deferred work can continue afterward.
Which should I use: Playwright or Puppeteer?
Use the library that fits your project and browser requirements. Playwright supports Chromium, Firefox, and WebKit in its documented workflow; Puppeteer’s screenshot guide shows its Page API with Chromium automation.
Can I guarantee a screenshot is identical every time?
Not from a network-idle wait alone. Dynamic page content and timing can change the result. A stable, explicit readiness state improves consistency, but the page itself can still vary.


