Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errors“Target closed” means Puppeteer lost the browser target (a page, context, or the browser connection) while a DevTools operation was still running. In multi-URL jobs, the dependable pattern is one long-lived browser, bounded concurrency, one page or isolated BrowserContext per capture, await on every navigation/evaluation/screenshot, and cleanup in finally blocks. Close a page only after its screenshot promise settles; close the browser only after every job has settled.
What the error actually means
Puppeteer talks to Chrome through the DevTools Protocol. A page is a target. If that target is destroyed, or the browser connection disappears, an operation such as goto, evaluate, waitForSelector or screenshot can reject with Target closed (also exposed by newer Puppeteer versions as a TargetCloseError).
The error describes the symptom, not the cause. The immediate cause is always that the target vanished before the pending protocol call completed. The underlying cause may be your lifecycle code, excessive parallelism, a browser crash, or an unsuitable host environment.
The safe lifecycle for a batch
One browser, controlled workers
A single Browser can own many Page instances, each with its own viewport. Reusing the browser avoids launching Chrome for every URL, but do not turn an array of URLs into an unbounded Promise.all. Every extra page consumes memory, file descriptors and renderer processes. Start with a small worker count (for example, two), observe CPU, memory and disconnects, then increase gradually.
#1 Best Overall
One page or context per job
Create a fresh page for each capture when jobs are independent. If cookies or local storage must not leak between URLs or tenants, create a separate BrowserContext and page; closing that context closes all of its pages. A shared page is possible, but every navigation and screenshot must be serialized and cleanup must be unambiguous.
Await before closing
page.close(), context closure, and browser.close() must happen after all protocol promises have resolved. During a screenshot, Puppeteer waits for newPage, Browser.newPage and Page.close as documented; bringToFront() does not wait for an existing screenshot. Never start a screenshot and immediately close its page.
A production-safe Puppeteer example
The following Node.js program uses a long-lived browser, a two-worker pool, explicit timeouts, event logging and guaranteed page cleanup. Replace the URLs and output names with your own.
import puppeteer from 'puppeteer';
const urls = [
'https://example.com',
'https://developer.chrome.com',
'https://puppeteer.github.io/puppeteer/'
];
const browser = await puppeteer.launch({
// Add dumpio: true while diagnosing Chrome crashes.
});
browser.on('disconnected', () => console.error('Browser disconnected'));
browser.on('targetdestroyed', target => console.error('Target destroyed:', target.url()));
async function capture(url, path) {
const page = await browser.newPage();
page.on('error', err => console.error('Page error:', url, err));
page.on('close', () => console.error('Page closed:', url));
page.on('console', msg => console.log(`[page ${url}] ${msg.type()}: ${msg.text()}`));
try {
await page.setViewport({ width: 1440, height: 900 });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 45_000 });
await page.screenshot({ path, fullPage: true });
} finally {
if (!page.isClosed()) await page.close();
}
}
async function runWithConcurrency(items, limit, worker) {
let next = 0;
async function run() {
while (true) {
const index = next++;
if (index >= items.length) return;
await worker(items[index], index);
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, run));
}
try {
await runWithConcurrency(urls, 2, (url, i) => capture(url, `shot-${i}.png`));
} finally {
await browser.close();
}
This shape separates job completion from browser shutdown. If one capture fails, its finally still closes the page; the outer finally still closes Chrome after workers settle.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #2
Choosing an isolation strategy
| Approach | Isolation | Resource use | Failure scope | Use when |
|---|---|---|---|---|
| Reuse one page | Shared cookies and storage | Lowest | One page can invalidate the next job | Captures are strictly sequential and state is intentionally shared |
| New page per URL | Separate page, usually shared browser profile | Moderate | Normally limited to one page | Independent screenshots in a bounded pool |
| New BrowserContext plus page | Cookies and local storage isolated | Higher than a page alone | Context jobs are separated; browser crash affects all | Multi-tenant work or privacy-sensitive sessions |
| New browser per URL | Complete process isolation | Highest and slowest | Usually one URL | Only when a hostile or unstable site requires process-level separation |
Why multiple-URL batches fail
Closing a page while work is pending
Calling page.close() or closing its context while goto, evaluate, a selector wait or screenshot is unresolved destroys the target. Keep the screenshot call itself inside the try, and close only in finally after it has been awaited.
Closing and reopening in a race
A long-running page.evaluate() can still be using the target when code closes the page and immediately calls browser.newPage(). A reported Puppeteer issue shows that arbitrary delays do not reliably cure this race. Await the evaluation, await closure, then verify browser health before creating the replacement page. If the browser disconnected, restart the job instead of reusing the destroyed page.
Unbounded parallelism
Promise.all(urls.map(capture)) can launch more renderers and screenshots than the host can sustain. Memory pressure may crash Chrome, destroying every target at once. A queue or worker pool makes concurrency an explicit operating limit.
Chrome or the host exits
Sandbox permissions, missing shared libraries, unsuitable container images, process limits, unwritable profile directories and zombie Chrome processes can cause startup or later crashes. The resulting target error may be reported far from the original environmental failure.
Recommended Free Tools
Diagnostics that identify the failing layer
- Listen for
browser.on('disconnected')to detect a closed or crashed browser. - Listen for
browser.on('targetdestroyed')and pageclose/errorevents to identify page-level destruction. - Run with
headless: falseorslowMoto watch navigation and closure timing. - Set
dumpio: trueto forward Chrome stderr. - Set
NODE_DEBUG="puppeteer:*"to inspect protocol traffic; inspectbrowser.debugInfo.pendingProtocolErrorsfor unresolved calls. - Forward page console messages, as in the example, to expose site scripts that trigger crashes or navigation.
- Check sandbox configuration, native libraries, process/file-descriptor limits, writable temporary and profile paths, and orphaned Chrome processes.
Recovery and retry policy
Retry only after classifying the failure. A navigation timeout may be retried in a new page. A closed target must not be reused. If disconnected fired, treat the browser as dead: terminate any remaining work, launch a new browser, and retry the whole affected job. Use a bounded retry count and record the URL, attempt, exception, browser-disconnect state and elapsed time. Do not hide a deterministic page error behind endless retries.
Make waits deterministic
Choose a navigation condition that matches the site. networkidle2 can remain pending on applications with long-lived connections; use a specific selector or a bounded delay after navigation when that is more appropriate. Always set finite timeouts. If you wait for a selector, ensure the page has not navigated or been closed between the wait and the next operation.
Keep capture work small
Large full-page screenshots increase memory use. Capture only the required element when possible, limit viewport and device scale, and process output before starting many more jobs. Blocking unnecessary resources can reduce renderer load, but verify that blocked assets do not change the screenshot you require.
Or skip the browser setup:
ScreenshotNeo provides a GET-based screenshot API and an MCP server for Claude, Cursor and other MCP clients. It accepts cookie and consent banners as a visitor, then removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers report the page verdict and billing status.
One request is enough:
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 complete parameter reference in the ScreenshotNeo documentation. The same endpoint supports PNG, JPEG or WebP, full-page capture with lazy images, CSS-selector elements, dark mode, 12 device presets or custom viewports, retina scale, PDF output, custom CSS and JavaScript, clicks, selector/delay/network-idle waits, blocked requests, headers, cookies, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting and an OpenAPI specification. Common screenshot-API parameter names also work when switching.
Rank #4
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}`);
The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan. Sign up free for ScreenshotNeo.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.FAQ
Does one Browser support parallel pages?
Yes. Puppeteer supports multiple Page instances in one Browser. Limit the number running simultaneously to what your host can sustain.
Does browser.disconnect() stop Chrome?
No. It detaches the client without shutting down the browser. Use browser.close() when your process owns Chrome and should terminate it.
Why does adding a sleep not fix the error?
A sleep does not prove that an outstanding protocol call, evaluation or browser process has finished. Await the operation and closure, then check browser connection state.
Best Value
Frequently Asked Questions
Can I safely reuse a page for every URL?
Yes, if captures are strictly sequential, all operations are awaited, and shared cookies and storage are acceptable. A fresh page per job is easier to reason about.
Should I increase concurrency when screenshots are slow?
Not automatically. Increase it only after observing stable memory, CPU, browser connectivity and output quality; higher concurrency can cause Chrome to crash.
What should I do after a browser disconnect?
Discard the browser and affected pages, start a new browser process, and retry the complete affected job with a bounded retry policy.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

