How Puppeteer Manages Browser Processes
Learn when Puppeteer launches or attaches to a browser, how to inspect process ownership, and when to close or disconnect safely.
Puppeteer manages browsers in two ways: it can start a browser with puppeteer.launch(), or connect to a browser that another process already started with puppeteer.connect(). If Puppeteer launched the browser, call await browser.close() to shut it down gracefully. If another service owns it, call browser.disconnect() to detach the Puppeteer client while leaving the browser running. A connected browser’s browser.process() is null; Puppeteer does not own its operating-system process.
The practical rule is to match cleanup to ownership. Closing a shared browser can disrupt other clients; disconnecting from a browser your application owns can leave a process running. This guide shows both lifecycles, process inspection, launch configuration, and common failure fixes.
1. Choose the browser ownership model
| Situation | Start or attach | Cleanup | Process behavior |
|---|---|---|---|
| Your Node.js app owns the browser lifecycle | puppeteer.launch() |
await browser.close() |
Gracefully closes the browser Puppeteer launched. |
| A browser service or separate process owns the lifecycle | puppeteer.connect() |
browser.disconnect() |
Detaches the client; browser and pages remain running. |
| You need the process handle for a launched browser | browser.process() |
Use the handle for inspection or process-level integration as needed | Returns a Node.js ChildProcess, or null for a connected instance. |
Decide based on three questions: who should restart the browser, whether ending this client should end the browser, and who controls the executable and version. These choices determine both startup and cleanup.
2. Launch and close a browser Puppeteer owns
Install Puppeteer in a Node.js project. The puppeteer package downloads a compatible browser by default; the examples below use that managed browser.
npm install puppeteer
Save this as capture.js and run node capture.js:
const puppeteer = require('puppeteer');
async function main() {
const browser = await puppeteer.launch({
headless: true,
timeout: 30_000,
});
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log('Browser PID:', browser.process()?.pid ?? 'unavailable');
console.log('Page title:', await page.title());
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
The finally block closes the browser whether navigation succeeds or throws. browser.close() is asynchronous, so await it before the Node.js process exits. This example uses CommonJS and works with a standard Node.js project; in an ES module, use import puppeteer from 'puppeteer'; instead.
The documented generic launch defaults include Chrome, headless mode, and a 30-second startup timeout, but defaults can change between releases. Set behavior explicitly when it matters and check the documentation for your installed version: LaunchOptions and PuppeteerNode.launch().
3. Connect to a browser owned by another process
The external launcher must provide a Puppeteer-compatible browser WebSocket endpoint. The endpoint is typically obtained from that browser service’s configuration or startup output; there is no universal endpoint to substitute. Keep credentials and endpoint details out of source control.
Install puppeteer-core when connecting to an externally managed browser without Puppeteer’s bundled browser:
npm install puppeteer-core
const puppeteer = require('puppeteer-core');
async function main() {
const browser = await puppeteer.connect({
browserWSEndpoint: process.env.BROWSER_WS_ENDPOINT,
});
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log('Connected browser title:', await page.title());
console.log('Owned child process:', browser.process()); // null
} finally {
browser.disconnect();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Set BROWSER_WS_ENDPOINT to the real endpoint supplied by your browser owner. The key lifecycle difference is deliberate: this code detaches and leaves the browser running. Do not replace disconnect() with close() unless your application is responsible for shutting down that browser.
4. Understand process inspection and browser binaries
browser.process()
For a browser started through launch(), browser.process() gives you its Node.js ChildProcess. For a browser reached through connect(), it returns null because the process belongs to the external launcher. Use this as an ownership clue, not as a replacement for the browser lifecycle API. See the Browser.process() reference.
Choosing a browser executable
Puppeteer recommends the Chrome for Testing version it downloads by default; it does not guarantee compatibility with arbitrary Chrome versions. If using puppeteer-core to launch rather than connect, supply executablePath or channel. For example:
const puppeteer = require('puppeteer-core');
const browser = await puppeteer.launch({
executablePath: process.env.CHROME_PATH,
headless: true,
});
try {
// Use browser pages here.
} finally {
await browser.close();
}
Set CHROME_PATH to an installed, compatible browser binary. Avoid assuming a system Chrome version works with every Puppeteer release. Puppeteer configuration can control browser downloads and cache behavior; the separate @puppeteer/browsers package supports installing, listing, resolving executable paths, launching, and uninstalling browser builds. See Configuration and the @puppeteer/browsers API.
5. Relevant launch options and shutdown behavior
Launch options govern how a browser process starts and how Puppeteer manages it. Commonly relevant settings include:
headless: select headless behavior. The generic current default is enabled, but set it explicitly for predictable deployment.executablePathorchannel: choose a browser binary or release channel. Withpuppeteer-core, one is required when launching.args: pass browser command-line arguments. Validate required arguments against the browser and environment; unnecessary flags can change security or rendering behavior.env: set the environment passed to the launched browser process.userDataDir: select a profile directory. Use care with concurrent launches that would share a profile.timeout: limit browser startup time; the documented generic default is 30 seconds.pipe: use pipe transport instead of a WebSocket transport where supported.handleSIGHUP,handleSIGINT,handleSIGTERM: these signal handlers are enabled by default and close the browser on the corresponding signals.signal: provide anAbortSignal; aborting it can close the browser.
Option names and defaults are version-sensitive. Consult the official LaunchOptions reference for the installed version before relying on a default or tuning a deployment.
6. Handle shutdown reliably
Use one clear owner for shutdown. A common pattern is to create the browser inside a function, put work in try, and await browser.close() in finally. If the browser is shared, each client should disconnect; the service that launched it should decide when it is safe to close.
- Avoid closing a shared browser when another client may still use its pages.
- Avoid calling
disconnect()as cleanup for a browser your process must release; that can leave it running. - Do not add competing signal handlers without checking Puppeteer’s signal handling options. Duplicate cleanup can obscure which component owns shutdown.
- When using an external process manager, make its restart and termination policy agree with the application’s Puppeteer lifecycle.
7. Troubleshoot common process problems
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser remains after the script finishes | The launched browser was disconnected from, or close was not awaited. | For a browser launched by this app, put await browser.close() in a finally block. For an externally owned browser, confirm its owner is responsible for shutdown. |
| Other users lose their browser session | A client called close() on a browser that the client did not own. |
Connect with puppeteer.connect() and call browser.disconnect(). Have the external owner close the browser when appropriate. |
browser.process() returns null |
The browser was connected to and was launched elsewhere. | This is expected. Inspect or manage the process through the service that launched it. |
| Launch reports that no executable is configured | puppeteer-core was asked to launch without a browser path or channel. |
Set executablePath or channel, or use the puppeteer package and its downloaded browser. |
| Browser launches but fails or behaves unexpectedly | The selected system browser may be incompatible with the installed Puppeteer version. | Prefer Puppeteer’s downloaded Chrome for Testing build, or verify browser and Puppeteer compatibility for the chosen binary. |
| Startup times out | The browser cannot start within the configured timeout, often due to a missing binary, environment problem, or slow startup. | Check the executable path and environment first. Increase timeout only when slow startup is expected and the environment is otherwise valid. |
| Abort or termination unexpectedly closes a browser | An abort signal or enabled signal handler triggered shutdown. | Review signal and the SIGHUP/SIGINT/SIGTERM handling options, and assign one component clear responsibility for cleanup. |
8. Performance, reliability, and cost considerations
Process ownership is primarily a reliability decision. Reusing a browser managed by a separate service can avoid making each client responsible for startup and teardown, but it also means clients share a lifecycle boundary. Launching per task gives the application direct control of shutdown, while adding browser startup work. The right arrangement depends on isolation needs, workload, and who operates the browser.
Keep the selected browser binary available in the deployment environment and use a compatible version. Set a startup timeout that reflects the environment, and ensure failures still reach cleanup. Puppeteer and browser processes consume deployment resources; the cited documentation does not establish a universal resource or speed figure, so measure against your own pages and concurrency.
There is no ScreenshotNeo charge for using Puppeteer itself. If the task is simply to obtain a website screenshot and you do not need to operate a browser process, ScreenshotNeo is a hosted alternative: one GET request returns an image or PDF. It bills only clean shots; bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Free includes 1,000 shots per month without a card; paid plans start at $5 for 3,000.
Or skip the browser setup
For a screenshot without installing or managing a browser process, make one request to ScreenshotNeo. See the ScreenshotNeo API documentation for parameters and response behavior.
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(`ScreenshotNeo returned ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo accepts cookie consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the screenshot; you can turn each step off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Free includes 1,000 screenshots monthly with no card; paid plans start at $5 for 3,000. Learn about ScreenshotNeo, then sign up for 1,000 free screenshots a month, with no card.
FAQ
Does browser.disconnect() close pages?
It detaches Puppeteer from the browser. The browser and its pages continue running under the external owner’s lifecycle.
Can I get a PID from a connected browser?
Not through browser.process(); it returns null for connected instances. Ask the process or browser service owner for its process information.
Should I use puppeteer or puppeteer-core?
Use puppeteer when you want Puppeteer to manage its downloaded browser. Use puppeteer-core when you manage the browser binary or connect to an externally managed browser.
Will arbitrary Chrome versions work?
Compatibility is not guaranteed for arbitrary versions. Puppeteer recommends the Chrome for Testing build it downloads by default.


