Connecting to External Browsers
Connect over the Playwright protocol, or over CDP for Chromium.
Most projects should let Playwright PHP launch browsers itself. Use external browser connections only when the browser lifetime belongs somewhere else: a shared debugging browser, a remote worker, a browser server in Docker, or a Chromium instance started with DevTools enabled.
There are two connection modes:
connect()uses the Playwright protocol and expects awsEndpoint()fromlaunchServer(). Prefer this for full Playwright behavior.connectOverCDP()attaches to a Chromium/Chrome DevTools endpoint. Use it when you already have Chrome running with--remote-debugging-port.
Choose the connection mode deliberately
Use a local launch for normal tests. It is simpler, isolated, and easier to clean up.
Use launchServer() plus connect() when another process owns the browser server but you still want Playwright protocol behavior. Typical cases are Docker workers, shared debug infrastructure, or a long-running browser service used by short PHP jobs.
Use connectOverCDP() only for Chromium-based browsers that already expose a DevTools endpoint. It is practical for local inspection and some hosted browser providers, but it is not a general replacement for Playwright’s protocol.
The choice affects debugging. With a local launch, the PHP test controls the whole lifecycle. With a remote endpoint, failures may belong to the network, provider, browser server, browser context, or page. Keep the endpoint creation step visible.
Playwright protocol: recommended
Start a browser server in one PHP process:
<?php
require __DIR__.'/vendor/autoload.php';
use Playwright\PlaywrightFactory;
$playwright = PlaywrightFactory::create();
$server = $playwright->launchServer('chromium', ['headless' => true]);
echo $server->wsEndpoint().PHP_EOL;
while (true) {
sleep(1);
}
Copy the full ws://... endpoint, including the path segment. Then connect from another script:
<?php
require __DIR__.'/vendor/autoload.php';
use Playwright\PlaywrightFactory;
$endpoint = getenv('REMOTE_WS_ENDPOINT');
if (false === $endpoint || '' === $endpoint) {
throw new RuntimeException('Set REMOTE_WS_ENDPOINT to the wsEndpoint() value.');
}
$playwright = PlaywrightFactory::create();
$browser = $playwright->chromium()->connect($endpoint);
$context = $browser->newContext();
$page = $context->newPage();
$page->goto('https://example.com');
echo $page->title().PHP_EOL;
$context->close();
$browser->close();
$playwright->close();
connect() closes your client connection when $browser->close() runs. It does not necessarily mean your long-lived server process has stopped; stop the process that owns launchServer() when you are done.
CDP: Chromium only
Start Chrome with remote debugging. On macOS:
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-pw
Then connect over CDP:
<?php
require __DIR__.'/vendor/autoload.php';
use Playwright\PlaywrightFactory;
$playwright = PlaywrightFactory::create();
$browser = $playwright->chromium()->connectOverCDP('http://127.0.0.1:9222');
$context = $browser->newContext();
$page = $context->newPage();
$page->goto('https://example.com');
echo $page->url().PHP_EOL;
$browser->close();
$playwright->close();
CDP is useful for inspecting an already-running local Chrome. It is not a cross-browser protocol and should not be your default CI setup.
Context ownership still matters
Connecting to an external browser does not remove the need for context isolation. Create a fresh context for each scenario unless you intentionally test shared state.
$browser = $playwright->chromium()->connect($endpoint);
$context = $browser->newContext([
'locale' => 'en-US',
'timezoneId' => 'UTC',
]);
try {
$page = $context->newPage();
$page->goto('https://app.example.test');
echo $page->title().PHP_EOL;
} finally {
$context->close();
$browser->close();
$playwright->close();
}
This pattern makes ownership explicit. The browser may be remote, but the test still owns the context and page it creates. Closing the context also removes cookies, local storage, permissions, and pages created for that scenario.
Remote endpoints and secrets
Some hosted browser services encode authentication in the WebSocket URL. Treat that URL like an API token. Put it in CI secrets, avoid printing the full value, and never paste it into an issue with logs.
Also check where the browser runs geographically and logically. If the remote browser cannot reach http://127.0.0.1:8000, your PHP test will connect successfully but goto() will fail. For local apps, expose a tunnel or run the browser near the app. For private staging apps, make sure allowlists include the browser host, not only the PHP runner.
When external browsers are the wrong tool
Do not introduce a remote browser just to make local failures look more like CI. It adds another network hop, another process owner, and another place for credentials to leak. First make the local launch deterministic: fixed viewport, clean context, explicit base URL, and useful failure artifacts.
Avoid sharing one external browser across many parallel jobs unless each job creates its own context and the provider supports the load. A shared browser can make tests influence one another through CPU pressure, downloads, video storage, or leftover pages. If the goal is speed, measure it against the added flakiness cost.
External connection is strongest when it solves a real constraint: the browser must run in a locked-down network, the test runner cannot install browser binaries, a debugging session must attach to an existing Chrome, or a provider offers browsers your CI image cannot host.
Common mistakes
- Passing
http://127.0.0.1:9222toconnect(). That is a CDP URL; useconnectOverCDP(). - Trimming the
wsEndpoint()path. The random-looking path is part of the endpoint. - Using
localhostwhen only IPv4 is listening. Try127.0.0.1. - Forgetting that remote browsers need the same network access as the browser process, not the PHP process.
- Sharing one context between unrelated tests. Even with a remote browser, create a fresh context per scenario.
Treat external endpoints as secrets when they point outside your machine. A WebSocket endpoint can grant control of the browser, including its pages and storage. Keep it on a private network, prefer short-lived servers, and avoid printing hosted provider URLs in public CI logs.
Verification and debugging
First verify the endpoint with a tiny script that prints $page->title(). If connection fails, confirm the server process is still alive, the port is reachable from the PHP host, and the endpoint type matches the method. In CI or Docker, log the endpoint host and port, but avoid printing credentials if your remote service embeds them in the URL.
When debugging containers, remember that 127.0.0.1 means "inside this container". If PHP runs in one container and the browser server in another, connect through the service name or published host port instead.
Go next
- Choose local lifecycle and isolation: Browsers and contexts
- Diagnose the connection: Debugging
- Run the endpoint in automation: Continuous integration
- Copy a complete script: Connect to an external browser, connect to a remote browser, or run a browser server