How to Connect Pyppeteer to an Existing Chrome Browser
Attach Pyppeteer to a running Chrome with remote debugging, find its browser WebSocket endpoint, and troubleshoot common connection failures.
To connect Pyppeteer to an already running Chrome process, start Chrome with remote debugging enabled, read the browser-level WebSocket endpoint, and pass that endpoint to pyppeteer.connect(). The endpoint must look like ws://host:port/devtools/browser/<id>; a plain port URL and a page-level WebSocket URL are different values.
What you need
- Python and a compatible Pyppeteer installation.
- A Chrome or Chromium process started with remote debugging enabled.
- Network access from your Python process to Chrome’s debugging port.
- The complete browser WebSocket URL, including its
/devtools/browser/...path.
Pyppeteer’s API reference documents connect() for attaching to an existing Chrome process. It also notes that Pyppeteer works best with its bundled Chromium and does not guarantee compatibility with arbitrary Chrome or Chromium versions. Record both versions when diagnosing problems. See the Pyppeteer API reference.
1. Start Chrome with remote debugging
Chrome must expose a DevTools Protocol endpoint before another process can attach. Close the normal Chrome instance first, then start a separate profile with a debugging port. Using a separate profile avoids locking or changing your everyday profile.
Linux
google-chrome \\
--remote-debugging-port=9222 \\
--user-data-dir=/tmp/pyppeteer-chrome-profile
macOS
/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome \\
--remote-debugging-port=9222 \\
--user-data-dir=/tmp/pyppeteer-chrome-profile
Windows PowerShell
& "$env:ProgramFiles\\Google\\Chrome\\Application\\chrome.exe" `
--remote-debugging-port=9222 `
--user-data-dir="$env:TEMP\\pyppeteer-chrome-profile"
The port is only an example. If another process already uses 9222, choose an unused local port and use the same value when retrieving the endpoint.
Chromium’s web-testing guidance covers enabling and forwarding the debugging port. For a remote machine, keep Chrome bound to a protected interface and use an SSH tunnel instead of exposing the debugging service to arbitrary clients.
2. Retrieve the browser WebSocket endpoint
Once Chrome is running, inspect its debugging version endpoint:
curl http://127.0.0.1:9222/json/version
The response contains a webSocketDebuggerUrl value similar to:
{
"Browser": "Chrome/…",
"webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/browser/9f2…"
}
Copy the complete webSocketDebuggerUrl. The identifier is generated by the running browser, so do not copy the placeholder from an example. A URL such as http://127.0.0.1:9222 is the debugging HTTP origin, not the value Pyppeteer expects.
3. Connect with Pyppeteer
Install Pyppeteer in the environment that will run your script:
python -m pip install pyppeteer requests
Then pass the browser-level endpoint to connect():
import asyncio
from pyppeteer import connect
BROWSER_WS = "ws://127.0.0.1:9222/devtools/browser/your-browser-id"
async def main():
browser = await connect({
"browserWSEndpoint": BROWSER_WS,
})
pages = await browser.pages()
print(f"Connected; open pages: {len(pages)}")
if pages:
page = pages[0]
print("Title:", await page.title())
print("URL:", page.url)
# Detach while leaving the externally managed Chrome process running.
await browser.disconnect()
asyncio.run(main())
Replace your-browser-id with the ID returned by /json/version. browser.disconnect() is the cleanup operation for an attachment workflow: it closes Pyppeteer’s connection without treating the externally managed Chrome process as owned by your script.
4. Select and control existing tabs
browser.pages() returns pages that are already open. You can inspect them, choose one by URL, or create a new tab inside the connected browser.
import asyncio
from pyppeteer import connect
async def main():
browser = await connect({
"browserWSEndpoint": "ws://127.0.0.1:9222/devtools/browser/your-browser-id"
})
pages = await browser.pages()
target = next((p for p in pages if "example.com" in p.url), None)
if target is None:
target = await browser.newPage()
await target.goto("https://example.com", {"waitUntil": "networkidle2"})
print(await target.title())
await browser.disconnect()
asyncio.run(main())
Existing tabs retain the session state held by that Chrome profile, including cookies and authenticated sessions. That is the main reason to attach instead of launching a fresh browser. Treat that state as sensitive.
Connection options that matter
| Option or choice | Use |
|---|---|
browserWSEndpoint |
Required browser-level WebSocket URL returned by Chrome’s debugging endpoint. |
connect() |
Attaches to a process that is already running. |
launch() |
Starts a new browser process managed by Pyppeteer. |
browser.disconnect() |
Detaches and leaves the external browser running. |
Separate --user-data-dir |
Prevents profile-lock conflicts and isolates automation state. |
| SSH port forwarding | Provides a protected path to a browser on another machine. |
Use connect() when an existing profile, tab, login, extension, or manually started browser must remain available. Use launch() when your program should own the browser lifecycle and can start a clean instance itself. Neither mode is universally more reliable; compatibility between your Pyppeteer version and Chrome version still matters.
Remote connections and security
A DevTools endpoint grants powerful control over the browser, including access to pages and session data. Keep the debugging port local whenever possible. Do not publish it directly to the internet or an untrusted network.
For a browser on another host, forward the port over SSH:
ssh -N -L 9222:127.0.0.1:9222 user@remote-host
Your local script can then use the forwarded address:
ws://127.0.0.1:9222/devtools/browser/your-browser-id
Chromium documents port-forwarding approaches for remote testing. General CDP guidance also warns that a network-accessible debugging interface can expose browser RPC to anyone who can reach it; apply firewall rules and authentication at the network boundary.
Common errors and fixes
“Connection refused”
Cause: Chrome is not running with remote debugging, the port is wrong, or Chrome is listening in another network namespace.
Fix: Confirm the process command line includes --remote-debugging-port. Run curl http://127.0.0.1:9222/json/version from the same environment as Python. In containers or VMs, verify that the port is forwarded and the address is reachable.
“Invalid websocket URL” or a handshake failure
Cause: The script received the HTTP origin, a page WebSocket URL, or an incomplete browser URL.
Fix: Read webSocketDebuggerUrl from /json/version and pass the entire value, including /devtools/browser/<id>. The browser-level path is required.
The endpoint worked once, then stopped working
Cause: Chrome restarted, so its browser ID changed, or the debugging process was replaced.
Fix: Fetch /json/version again immediately before connecting instead of storing a stale ID. If you manage Chrome yourself, monitor its process and restart it with the expected profile and port.
No pages are returned
Cause: Chrome is running but has no open tabs, or the connection is pointed at a different browser instance.
Fix: Open a tab in the profile, call browser.newPage(), and print each page’s URL. Check that the profile directory and port belong to the intended process.
Navigation or JavaScript behaves differently
Cause: The installed Chrome version, the Pyppeteer release, and the page’s browser requirements may not align. Pyppeteer’s documentation gives no guarantee for arbitrary Chrome or Chromium versions.
Fix: Record the output of google-chrome --version (or the platform equivalent) and your Pyppeteer version. Reproduce with Pyppeteer’s bundled Chromium where practical, or choose versions known to work together. Also inspect the existing profile’s extensions, policies, proxy settings, and authentication state.
It works locally but not in Docker or CI
Cause: “127.0.0.1” refers to the container or runner, not the host where Chrome runs; the port may not be published or forwarded.
Fix: Put both processes in the same network namespace, publish the debugging port deliberately, or use an SSH tunnel. Test the version endpoint from inside the exact runtime that executes Python.
Performance, reliability, and cost considerations
- Reuse the connection: Keep one connected browser for a batch of related operations instead of reconnecting for every page.
- Reuse tabs carefully: Closing or recycling tabs limits memory growth, but preserve tabs that contain required session state.
- Use an isolated profile: A dedicated profile reduces interference from manual browsing, extensions, and profile locks.
- Expect endpoint changes: A browser restart can invalidate the WebSocket URL. Fetch a fresh URL after a restart.
- Measure the real bottleneck: Connection setup is usually small compared with page navigation, scripts, fonts, images, and network waits. Set explicit navigation and application-level timeouts.
- Control concurrency: Too many simultaneous tabs can exhaust CPU, memory, file descriptors, or the remote host’s network capacity.
- Protect credentials: An attached profile may contain cookies and tokens. Do not log WebSocket URLs, page contents, or profile directories in shared systems.
Launch versus attach checklist
| Question | Attach with connect() |
Launch with launch() |
|---|---|---|
| Must existing tabs remain open? | Yes | No |
| Must existing cookies or login state be reused? | Yes | Only if you configure a matching profile |
| Can your program start Chrome? | Not required | Required |
| Is a protected debugging endpoint available? | Required | Managed internally |
| Who owns browser shutdown? | The external process owner | Usually Pyppeteer |
Or skip the browser setup
If your goal is simply to turn a URL into a clean screenshot, ScreenshotNeo provides a single HTTP request instead of a Chrome process, profile, debugging port, and WebSocket lifecycle. See the ScreenshotNeo API docs.
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 the shot. Bot checks, blank pages, and failed loads are never billed, and response headers report the page verdict and billing result. Its MCP server lets AI agents take screenshots, inspect pages, and capture PDFs. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Can I pass http://127.0.0.1:9222 directly to Pyppeteer?
No. Use the browser WebSocket URL from /json/version, including its /devtools/browser/<id> path.
Should I use a page WebSocket URL?
No. Pyppeteer’s documented connection value is the browser-level endpoint.
Will disconnecting close Chrome?
browser.disconnect() is intended to detach while leaving the externally managed process running. Verify behavior against your installed Pyppeteer version before using other shutdown methods.
Can I connect to a browser on another machine?
Yes, if the endpoint is reachable, but use a protected tunnel such as SSH forwarding and restrict the debugging port.
Why does the same script fail with a different Chrome installation?
Pyppeteer’s compatibility is strongest with its bundled Chromium and is not guaranteed for every Chrome or Chromium release. Compare browser and Pyppeteer versions when troubleshooting.


