How to Fix Pyppeteer Connections Closing After a Minute
A connection that closes after about a minute has no single Pyppeteer fix. Identify which process, WebSocket, task, or intermediary closed first.
A Pyppeteer connection that closes after roughly one minute is a symptom, not a diagnosis. There is no documented universal one-minute timeout that closes every Pyppeteer connection. The component that closed may be the Chromium process, Chrome DevTools WebSocket, Python WebSocket client, event loop task, proxy, container, or another network boundary.
Start by capturing the first exception and checking whether Chromium is still alive. Increasing a navigation timeout only helps when navigation itself timed out; it cannot reopen a WebSocket that has already closed.
What can close
| Component | What to check | Typical evidence |
|---|---|---|
| Chromium process | Process list, exit code, browser stderr, container limits | Browser disappears or reports a crash |
| DevTools WebSocket | Endpoint URL, close code, proxy and firewall path | WebSocket connection is closed or transport error |
| Python client | Pyppeteer and websockets versions |
Client-side connection exception while Chrome remains alive |
| Application task or event loop | Cancellation, shutdown hooks, worker lifetime | Disconnect occurs when a task, request, or process ends |
| Intermediary | Reverse proxy, container network, idle policy, NAT | Disconnect correlates with idle time or a hop in the network path |
Diagnostic sequence
- Record Python, Pyppeteer, Chromium, and
websocketsversions. Also record the operating system, container or proxy setup, and whether the program useslaunch()orconnect(). - Save the complete traceback and logs immediately before the close. The first transport exception is more useful than a later cleanup exception.
- At the failure time, check whether the Chromium process is still running.
- If Chromium exited, investigate process termination, memory or CPU limits, application cleanup, and browser logs.
- If Chromium remains alive, verify that the DevTools WebSocket endpoint can still be reached from the Python process.
- If using
connect(), confirm thatbrowserWSEndpointbelongs to the live browser instance. - Reproduce with Pyppeteer’s bundled Chromium when possible, then compare with the custom executable.
- Only change an operation timeout after confirming that the transport remains healthy and the operation itself is timing out.
Build a minimal reproduction
Reduce the program to one browser, one page, and one operation. Keep the browser open long enough to observe the failure and print the browser process state when an exception occurs.
import asyncio
import os
import platform
import sys
import time
import pyppeteer
from pyppeteer import launch
async def main():
print("python:", sys.version)
print("platform:", platform.platform())
print("pyppeteer:", getattr(pyppeteer, "__version__", "unknown"))
browser = await launch(
headless=True,
handleSIGINT=False,
handleSIGTERM=False,
handleSIGHUP=False,
dumpio=True,
)
try:
page = await browser.newPage()
await page.goto("https://example.com", {
"waitUntil": "networkidle2",
"timeout": 60000,
})
print("title:", await page.title())
print("browser process:", browser.process.pid if browser.process else None)
await asyncio.sleep(90)
print("still connected")
except Exception as exc:
print("first exception:", repr(exc))
process = browser.process
print("browser process:", process.pid if process else None)
if process:
print("process poll:", process.poll())
raise
finally:
try:
await browser.close()
except Exception as cleanup_exc:
print("cleanup exception:", repr(cleanup_exc))
if __name__ == "__main__":
asyncio.run(main())
Run this outside your web framework and outside a worker pool first. If it succeeds, add your application layers back one at a time. If it fails, the captured versions, close code, process state, and timing provide the information needed for the next step.
Launch mode versus connect mode
When you use launch()
Pyppeteer starts Chromium and owns its process. Check for an external shutdown, a parent process exiting, a container sending a termination signal, an out-of-memory kill, or a browser crash. Keep dumpio=True during diagnosis so Chromium stderr is visible.
Do not close the browser in a request-finally block while another task still uses a page. In asynchronous applications, make browser ownership explicit: create one long-lived browser per worker, give each operation its own page, and close the browser only during worker shutdown.
When you use connect()
connect() attaches to an existing browser through its DevTools WebSocket endpoint. The endpoint must point to the live browser instance. A stale endpoint, a browser restarted behind a proxy, or an intermediary that drops idle WebSocket connections can all look like a Pyppeteer timeout.
import asyncio
from pyppeteer import connect
async def main():
browser = await connect(
browserWSEndpoint="ws://127.0.0.1:9222/devtools/browser/YOUR_ID"
)
try:
page = await browser.newPage()
await page.goto("https://example.com", {"waitUntil": "domcontentloaded"})
print(await page.title())
finally:
await browser.disconnect()
asyncio.run(main())
Use the exact endpoint emitted by the running browser. If the browser is remote, test reachability from the same host and network namespace as Python. A successful HTTP health check to another port does not prove that the WebSocket path works.
Timeouts: what they fix and what they do not
Pyppeteer operation timeouts apply to actions such as navigation, selector waits, and script evaluation. They give a slow page more time. They do not repair a closed DevTools connection.
await page.setDefaultNavigationTimeout(120000)
await page.setDefaultTimeout(30000)
await page.goto(
"https://example.com",
{"waitUntil": "networkidle2", "timeout": 120000},
)
Use a longer timeout only when logs show that the page is still connected and the operation is genuinely slow. If the error is ConnectionClosed, WebSocket connection is closed, or similar, investigate transport and process state instead.
Compatibility and dependency checks
The Pyppeteer documentation describes Pyppeteer as working best with its bundled Chromium and does not guarantee compatibility with arbitrary Chromium versions. Reproduce with the bundled browser before changing multiple dependencies.
python -m pip show pyppeteer websockets
python --version
which chromium chromium-browser google-chrome 2>/dev/null || true
Historical issue reports include closures associated with particular environments and dependency versions, including a report involving websockets 7.0. That is evidence of a historical compatibility report, not a universal version pin. Change one dependency at a time in a controlled environment, record the result, and keep the known-good lockfile.
Event loops, cancellation, and application shutdown
A browser can appear to disconnect when the real cause is that its event loop or owning task ended. Common examples include calling asyncio.run() repeatedly inside a service, cancelling a task that owns the browser, returning from a worker before awaited page operations finish, or closing the loop while cleanup callbacks are still running.
- Create the browser inside the same long-lived loop that uses it.
- Await every page operation before returning from the task.
- Handle cancellation, then close or disconnect the browser once.
- Do not share a page between unrelated concurrent tasks.
- Keep cleanup exceptions separate from the initiating exception in logs.
One reported failure sequence showed ConnectionClosedOK followed by InvalidStateError during cleanup. Treat the first close as the root diagnostic event unless other evidence points elsewhere.
Proxies, containers, and idle connections
If the close happens after a consistent idle period, inspect every intermediary between Python and Chromium. Reverse proxies and load balancers may have WebSocket idle policies. Container supervisors may enforce memory, CPU, PID, or wall-clock limits. NAT devices and firewalls can expire idle flows.
- Test Python directly against Chromium on the same host.
- Bypass the proxy for a controlled reproduction.
- Compare an idle browser with one that performs a small operation periodically.
- Check container events, kernel logs, and memory pressure at the close time.
- Configure WebSocket upgrade and idle handling according to your intermediary’s documentation.
Do not add heartbeat code before proving that an intermediary is expiring idle connections. A heartbeat cannot fix a browser crash or an application task that has already been cancelled.
Common errors and fixes
| Error or symptom | Likely area | Action |
|---|---|---|
connection unexpectedly closed |
Transport, browser, or intermediary | Capture the first close code; check Chromium process and endpoint reachability. |
WebSocket connection is closed |
DevTools transport | Verify the endpoint, proxy path, and browser lifetime; do not only raise navigation timeout. |
Websocket connection is lost |
Remote browser or network boundary | Reproduce locally, then inspect proxy, container, firewall, and idle policies. |
| Close occurs when a request finishes | Application ownership | Move browser lifetime out of the request and await all tasks before cleanup. |
Cleanup raises InvalidStateError |
Secondary cleanup failure | Find the preceding transport exception and process event; do not treat cleanup as the root cause. |
| Works with bundled Chromium but not custom Chrome | Compatibility | Compare browser versions and flags; use the bundled browser or validate the custom pair. |
| Browser disappears in a container | Resource or supervisor limit | Inspect OOM, PID, CPU, and termination events and browser stderr. |
Reliability and performance practices
- Reuse a browser process when appropriate, but create isolated pages for concurrent jobs.
- Set explicit navigation and selector timeouts based on the page workload.
- Capture structured logs containing URL, operation, elapsed time, exception, close code, browser PID, and dependency versions.
- Retry only after a transport-level failure, and create a fresh page or browser for the retry.
- Use bounded concurrency so Chromium is not starved of memory or file descriptors.
- On repeated browser crashes, recycle the process instead of endlessly retrying the same instance.
Retries improve recovery from transient transport failures but can duplicate side effects on pages that submit forms or trigger actions. Make retries idempotent and limit their count.
Cost and operational notes
Self-hosting Pyppeteer means operating Python, Chromium, browser dependencies, process supervision, and any proxy path. Your main costs are compute, memory, storage, and engineering time. A longer timeout can increase resource occupancy without improving reliability if the underlying transport is already closed.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF, so your application does not need to keep a Pyppeteer browser and DevTools connection alive.
See the ScreenshotNeo API documentation for the available parameters.
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}`);
Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor, and other MCP clients 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 one minute a built-in Pyppeteer limit?
No universal one-minute limit is established by the available documentation or issue reports. Measure which component closes and when.
Should I set the timeout to five minutes?
Only when a connected page operation is slow. A larger operation timeout does not reopen a closed WebSocket.
Should I pin websockets to an old release?
Do not apply a blind pin. A historical report involved websockets 7.0, but current compatibility depends on your full environment.
What information should I include in a bug report?
Include the first traceback, Python, Pyppeteer, Chromium, and websockets versions, launch or connect mode, browser process state, endpoint setup, container or proxy details, and the exact elapsed time before the close.
When should I replace Pyppeteer?
Consider another capture architecture when you do not need an in-process browser and want an API that handles browser lifecycle, consent UI, failed loads, and billing decisions for you.


