How to Check Whether a Pyppeteer Browser Has Closed
Use the disconnected event for browser shutdowns, process polling for local launches, and page.isClosed() only for individual tabs.
Use Browser.Events.Disconnected to detect that a Pyppeteer browser connection ended asynchronously. For a synchronous check on a browser launched locally, inspect browser.process.poll(). Use page.isClosed() only to check one page or tab.
Choose the check that matches what you need
| Question | Check | What it tells you |
|---|---|---|
| Did the browser connection end? | Browser.Events.Disconnected |
An event is emitted when the DevTools connection closes. |
| Is a locally launched Chromium process still running? | browser.process.poll() |
None means the process is still running; a return code means it exited. |
| Did this tab close? | page.isClosed() |
The state of one page, not the entire browser. |
A browser can contain several pages. Closing one page does not close the browser, so a page check cannot be used as a browser-wide health signal.
Detect browser shutdown with the disconnected event
Register the handler immediately after launch() or connect(). This is the authoritative asynchronous signal for the client connection.
import asyncio
from pyppeteer import launch
from pyppeteer.browser import Browser
async def main():
browser = await launch()
def on_disconnected():
print('Pyppeteer browser disconnected')
browser.on(Browser.Events.Disconnected, on_disconnected)
page = await browser.newPage()
await page.goto('https://example.com', {'waitUntil': 'networkidle2'})
print(await page.title())
await browser.close()
if __name__ == '__main__':
asyncio.run(main())
The handler runs when the DevTools connection ends, whether the browser was closed by your code or disappeared unexpectedly. Keep the callback small and move shared cleanup into an idempotent function if several tasks can observe the shutdown.
Check a locally launched process synchronously
browser.process is available when Pyppeteer launched the browser. Call poll() to inspect the process without blocking:
proc = browser.process
browser_is_running = proc is not None and proc.poll() is None
if browser_is_running:
print('Chromium is still running')
else:
print('Chromium has exited or no local process is available')
A process return code means Chromium has exited. A None process is expected when the browser was obtained with connect(); in that deployment mode, rely on the disconnected event and operation errors instead.
Check whether one page closed
if page.isClosed():
print('This page is closed')
else:
print('This page is open')
This method answers a tab-level question only. It does not prove that other pages are open or that the browser connection is healthy.
Use all three signals in a lifecycle-aware worker
The following pattern records disconnection, polls a locally owned process when possible, and treats protocol failures during page work as a reason to stop using the session.
import asyncio
from contextlib import suppress
from pyppeteer import launch
from pyppeteer.browser import Browser
async def capture_with_health_check(url):
browser = await launch()
disconnected = asyncio.Event()
def mark_disconnected():
disconnected.set()
print('Browser connection ended')
browser.on(Browser.Events.Disconnected, mark_disconnected)
try:
page = await browser.newPage()
await page.goto(url, {'waitUntil': 'networkidle2'})
if disconnected.is_set() or page.isClosed():
raise RuntimeError('Browser became unavailable during navigation')
return await page.screenshot({'path': 'shot.png', 'fullPage': True})
except Exception as exc:
proc = browser.process
process_running = proc is not None and proc.poll() is None
print(f'Page operation failed: {exc}')
print(f'Local browser process running: {process_running}')
raise
finally:
# close() is safe as the final owner cleanup; suppress a race with exit.
with suppress(Exception):
await browser.close()
asyncio.run(capture_with_health_check('https://example.com'))
Use an idempotent cleanup path when multiple coroutines share a browser. A disconnect callback may run while an in-flight navigation is raising an exception.
close() versus disconnect()
await browser.close()closes connections and terminates a browser process that your code owns.await browser.disconnect()drops the client connection while leaving the remote browser running. Use this when another service owns the browser lifecycle.
After either operation, do not reuse the old browser object for new page work. Create a new connection or launch a new browser.
Race conditions and failed operations
A browser can disappear between a health check and the next command. Therefore, a successful poll() or isClosed() result is only a point-in-time observation. Wrap navigation, evaluation, screenshots and other protocol calls in try/except. If a protocol or connection error occurs, mark the session unusable, let the shared cleanup run, and decide whether to retry with a fresh browser.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
page.isClosed() is false, but commands fail |
The browser connection ended after the check. | Handle the operation exception and use the disconnected event as the session-wide signal. |
browser.process is None |
The browser came from connect(), not launch(). |
Do not poll a local process; monitor Browser.Events.Disconnected. |
| The disconnected callback never runs | The handler was registered after the connection had already ended, or the event loop stopped. | Register immediately after launch/connect and keep the event loop alive while work is running. |
| Cleanup raises another protocol error | A disconnect raced with browser.close(). |
Make cleanup idempotent and suppress or classify expected close-race errors. |
| Only one tab disappeared | A page was closed while the browser remained connected. | Use page.isClosed() for that tab and keep the browser session if other pages are healthy. |
| Retries reuse a broken browser | The old object remains in a failed state. | Discard it and launch or connect again before retrying. |
Performance and reliability guidance
- The disconnected event avoids polling loops and reacts as soon as the connection closes.
process.poll()is a cheap synchronous check, but it applies only to a locally launched browser.- Do not poll aggressively in a tight loop. Use the event for normal monitoring and poll only at decision points such as before a retry.
- Limit the number of pages per browser according to your workload, and recycle a browser after repeated protocol failures.
- Record whether the failure was a page close, browser disconnect or process exit; those signals lead to different recovery actions.
- Pin the Pyppeteer version or fork you deploy. The project documentation describes Pyppeteer as an unofficial Python port of Puppeteer, and its repository says it is unmaintained and has been outside minor changes for a long time. Verify the behavior of the exact dependency version in production.
Or skip the browser setup
If your goal is a reliable screenshot rather than managing Chromium, ScreenshotNeo provides a website screenshot API. See the API documentation for all options.
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}`);
ScreenshotNeo removes cookie banners, newsletter popups and chat widgets before capture. Bot checks, blank pages and failed loads are not billed, and response headers identify the page verdict and billing result. Its MCP server lets AI agents use take_screenshot, get_page_info and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Is page.isClosed() enough to detect a dead browser?
No. It reports one page’s state. Use the browser disconnected event for the connection and process polling for a locally launched Chromium process.
Can I use browser.process.poll() after connect()?
No. A connected remote browser has no local process object. Monitor disconnection and catch protocol errors.
Should I call close() or disconnect()?
Call close() when your code owns the launched browser. Call disconnect() when you only want to drop the client connection and leave the remote browser running.
What should a retry do after a disconnect?
Discard the unusable browser object, create a fresh launch or connection, and retry only operations that are safe to repeat.


