Browser Setup Guide
WebDriverBiDi.NET connects to a browser over a WebSocket (or, for Chromium, a pipe) that speaks WebDriver BiDi. The WebDriverBiDi package does not launch browsers: you start the browser, or its driver executable, and connect to the endpoint it reports, as this guide describes for each browser. To download and launch browsers from code instead, see the Dramaturge.Browsers package, part of the Dramaturge automation library built on WebDriverBiDi.NET.
Overview
BiDiDriver.StartAsync needs a WebSocket URL that speaks WebDriver BiDi. Which URL that is depends on the browser:
| Browser | Speaks BiDi natively? | How to get a BiDi endpoint |
|---|---|---|
| Chrome / Chromium / Edge | No. --remote-debugging-port and --remote-debugging-pipe expose the Chrome DevTools Protocol (CDP) only |
Create a session through chromedriver / msedgedriver with the webSocketUrl capability (recommended), or inject a BiDi-over-CDP mapper (advanced) |
| Firefox | Yes. --remote-debugging-port exposes BiDi at /session |
Connect directly, or go through geckodriver |
Important: the
ws://localhost:9222/devtools/browser/<id>URL that Chrome prints at startup and reports fromhttp://localhost:9222/json/versionis a CDP endpoint. ABiDiDriverconnected to it will open the socket successfully and then fail on its first command, because the browser does not understand WebDriver BiDi messages on that endpoint. Do not use it.
The library supports two connection types, both usable with any of the above:
- WebSocket Connection (default): connects to a WebSocket URL
- Pipe Connection: connects over anonymous pipes to a browser you launched with
--remote-debugging-pipe(Chromium only; see Connection Types)
Chrome / Chromium
Through chromedriver (recommended)
chromedriver hosts the BiDi implementation for Chrome: a classic WebDriver session created with the webSocketUrl: true capability comes back with a webSocketUrl that speaks WebDriver BiDi.
Get a chromedriver that matches your Chrome version from Chrome for Testing.
Start it:
chromedriver --port=9515Create a session, requesting
webSocketUrl. Browser flags (--headless=new,--user-data-dir=…,--no-sandbox, …) go ingoog:chromeOptions.args; chromedriver launches the browser for you:curl -X POST http://localhost:9515/session \ -H "Content-Type: application/json" \ -d '{"capabilities":{"alwaysMatch":{"webSocketUrl":true,"goog:chromeOptions":{"args":["--headless=new"]}}}}'The response contains the endpoint:
{ "value": { "sessionId": "8a4d1c2e-0b7f-4c9a-9d3e-5f6a7b8c9d0e", "capabilities": { "browserName": "chrome", "browserVersion": "131.0.6778.85", "webSocketUrl": "ws://localhost:9515/session/8a4d1c2e-0b7f-4c9a-9d3e-5f6a7b8c9d0e" } } }Doing the same from C#:
/// <summary> /// Creates a WebDriver session through a classic driver executable (chromedriver, msedgedriver, /// geckodriver) and returns the WebDriver BiDi WebSocket URL it advertises. /// </summary> public static async Task<(string SessionId, string WebSocketUrl)> CreateBiDiSessionAsync( string driverUrl = "http://localhost:9515", bool headless = false) { using HttpClient client = new HttpClient(); // "webSocketUrl": true asks the driver to expose the session over WebDriver BiDi. // Browser arguments go in the vendor-specific options (goog:chromeOptions here). string body = $$""" { "capabilities": { "alwaysMatch": { "webSocketUrl": true, "goog:chromeOptions": { "args": [{{(headless ? "\"--headless=new\"" : string.Empty)}}] } } } } """; using StringContent content = new StringContent(body, Encoding.UTF8, "application/json"); using HttpResponseMessage response = await client.PostAsync($"{driverUrl}/session", content); string json = await response.Content.ReadAsStringAsync(); response.EnsureSuccessStatusCode(); using JsonDocument doc = JsonDocument.Parse(json); JsonElement value = doc.RootElement.GetProperty("value"); string sessionId = value.GetProperty("sessionId").GetString() ?? throw new InvalidOperationException("The driver did not return a session ID."); string webSocketUrl = value.GetProperty("capabilities").TryGetProperty("webSocketUrl", out JsonElement url) ? url.GetString() ?? string.Empty : string.Empty; if (string.IsNullOrEmpty(webSocketUrl)) { throw new InvalidOperationException( "The driver did not return a webSocketUrl; the browser or driver may not support WebDriver BiDi."); } return (sessionId, webSocketUrl); }Connect to the
webSocketUrl:// The value of "webSocketUrl" from chromedriver's new-session response await driver.StartAsync("ws://localhost:9515/session/8a4d1c2e-0b7f-4c9a-9d3e-5f6a7b8c9d0e");
The session already exists, so do not call Session.NewSessionAsync on this connection. To finish, call driver.Session.EndAsync() (which also closes the browser) or DELETE http://localhost:9515/session/<sessionId>, then stop chromedriver.
Through a BiDi-over-CDP mapper (advanced)
The chromium-bidi project provides a JavaScript "mapper" that implements WebDriver BiDi on top of CDP. Injecting it into a hidden tab lets a client talk BiDi over the browser's own CDP endpoint (WebSocket or pipe) with no driver executable. The Dramaturge.Browsers package does exactly this in its ChromiumTransport (a Transport subclass that bootstraps the mapper during ConnectAsync), which its Chrome launcher uses; it is also a reference for building your own. This is the only route that works over a pipe connection, and it requires Session.NewSessionAsync after connecting because the mapper does not create a session.
Microsoft Edge
Edge is Chromium-based and follows the chromedriver path exactly, using msedgedriver (from the Edge WebDriver page) and ms:edgeOptions in place of goog:chromeOptions:
msedgedriver --port=9515
Connection Types
WebDriverBiDi.NET supports two transport mechanisms for communicating with browsers:
WebSocket Connection (Default)
How it Works:
- Your application connects to a WebDriver BiDi WebSocket URL: the
webSocketUrlof a session created through chromedriver/msedgedriver/geckodriver, or Firefox'sws://localhost:PORT/session - Works with local and remote endpoints
- Multiple clients can connect to the same driver or browser (each gets its own session)
Best For:
- Development and debugging
- Remote browser control
- Flexible deployment scenarios
- When you need to connect from outside your process
Example:
// chromedriver is already running: chromedriver --port=9515
(string sessionId, string webSocketUrl) = await CreateBiDiSessionAsync("http://localhost:9515");
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync(webSocketUrl);
// The driver created the session; do NOT call Session.NewSessionAsync here.
try
{
NavigateCommandResult result = await driver.BrowsingContext.NavigateAsync(navParams);
}
finally
{
// session.end closes the browser and releases the driver's session
await driver.Session.EndAsync();
await driver.StopAsync();
}
Pipe Connection
How it Works:
- A Chromium browser is launched with
--remote-debugging-pipe - Browser communicates via anonymous pipes (file descriptors 3 and 4 on Unix-like systems)
- Protocol uses null-terminated JSON messages
- The pipe carries CDP, so a BiDi-over-CDP mapper is required (see above); Firefox has no pipe mode
- Single client can connect (the process that launched the browser)
Best For:
- Automation frameworks
- Programmatic browser control
- Lower latency requirements
- When you control the browser lifecycle
Platform Support:
- Windows: Anonymous pipes
- macOS/Linux: File descriptor-based pipes
Example:
Note: The
WebDriverBiDipackage does not include a browser launcher. The example below usesMyChromiumPipeLauncher, a launcher of your own sketched after it; theDramaturge.Browserspackage's Chrome launcher, configured withWithConnection(ConnectionKind.Pipes), is a complete one. To write one, implementIPipeServerProcessProvider: launch the browser with--remote-debugging-pipeso that it inherits the two anonymous pipe handlesPipeConnectioncreates —PipeConnection.ReadPipeHandleandPipeConnection.WritePipeHandlegive you those handles as strings to pass to the child process, and both return an empty string once the connection has started, because the first start disposes the connection's local copies of those handles, so read them before callingStartAsync— and pass a mapper-installingTransportbuilt over thatPipeConnectiontoBiDiDriver:
// Your own IPipeServerProcessProvider, which launches Chromium with --remote-debugging-pipe and whose
// CreateTransport() installs a BiDi-over-CDP mapper (see Browser Setup).
MyChromiumPipeLauncher launcher = new MyChromiumPipeLauncher();
await launcher.StartAsync();
await launcher.LaunchBrowserAsync();
try
{
// Create driver with launcher's transport
BiDiDriver driver = new BiDiDriver(
TimeSpan.FromSeconds(30),
launcher.CreateTransport());
await driver.StartAsync("pipes");
// The mapper does not create a session; do it here
await driver.Session.NewSessionAsync(new NewCommandParameters());
// Use the driver
NavigateCommandResult result = await driver.BrowsingContext.NavigateAsync(navParams);
await driver.StopAsync();
}
finally
{
await launcher.QuitBrowserAsync();
await launcher.StopAsync();
}
The skeleton of your own IPipeServerProcessProvider implementation looks like this; PipeServerProcess returns the launched browser Process, and CreateTransport wraps a PipeConnection over this:
public class MyChromiumPipeLauncher : IPipeServerProcessProvider
{
public Process? PipeServerProcess => null; // Implement: launch browser process with pipe flags
// For a Chromium browser the pipe carries CDP, so the Transport returned here must translate
// WebDriver BiDi to CDP (see ChromiumTransport in the Dramaturge.Browsers
// library, which injects the chromium-bidi mapper). A plain Transport is shown only for shape.
public Transport CreateTransport() => new Transport(new PipeConnection(this));
public Task StartAsync() => Task.CompletedTask;
public Task LaunchBrowserAsync() => Task.CompletedTask;
public Task QuitBrowserAsync() => Task.CompletedTask;
public Task StopAsync() => Task.CompletedTask;
}
Comparison
| Feature | WebSocket | Pipes |
|---|---|---|
| Latency | Moderate (TCP overhead) | Lower (direct IPC) |
| Remote Access | ✓ Yes | ✗ No |
| Multi-Client | ✓ Yes | ✗ No |
| Setup Complexity | Simple | Moderate (mapper required) |
| Debugging | Easy (inspect traffic) | Moderate |
| Use Case | Development, debugging | Automation, testing |
Recommendation: Start with WebSocket connections through a driver executable for simplicity; switch to pipes only if you need lower latency and are prepared to host the mapper.
Firefox
Firefox implements WebDriver BiDi natively, so there are two ways in.
Direct: --remote-debugging-port
firefox --remote-debugging-port=9222
Firefox then serves WebDriver BiDi at ws://localhost:9222/session. No session exists yet, so create one after connecting:
// firefox --remote-debugging-port=9222 exposes WebDriver BiDi directly
await driver.StartAsync("ws://localhost:9222/session");
NewCommandResult sessionResult = await driver.Session.NewSessionAsync(new NewCommandParameters());
Through geckodriver
Download geckodriver from https://github.com/mozilla/geckodriver/releases
Launch geckodriver:
geckodriver --port 4444Either create a classic session with
webSocketUrl: trueexactly as for chromedriver (browser flags go inmoz:firefoxOptions.args) and connect to the returnedwebSocketUrl— noNewSessionAsyncneeded — or connect to geckodriver's BiDi-only endpoint and create the session yourself:await driver.StartAsync("ws://localhost:4444/session"); // No session exists yet on this endpoint; create one NewCommandResult sessionResult = await driver.Session.NewSessionAsync(new NewCommandParameters());
Important: On the
/sessionendpoints (Firefox direct, or geckodriver without a classic session) you must callSession.NewSessionAsyncafterStartAsync; without it, subsequent commands fail because no session exists on the remote end. On awebSocketUrlreturned by a classic new-session request the session already exists andNewSessionAsyncmust not be called.See the Session Module guide for full details and capability negotiation options.
Note on Firefox Support
Firefox's WebDriver BiDi implementation is actively being developed. Some features may not be available or may behave differently than in Chromium-based browsers.
Using with Selenium
If you already use Selenium, let it locate the driver and browser (Selenium Manager) and create the session; ask it for a BiDi WebSocket with UseWebSocketUrl, then connect a BiDiDriver to the webSocketUrl capability:
// Ask Selenium to request a WebDriver BiDi WebSocket for the session it creates.
// Selenium Manager locates (or downloads) chromedriver and the browser for you.
chromeOptions.UseWebSocketUrl = true;
ChromeDriver seleniumDriver = new ChromeDriver(chromeOptions);
string webSocketUrl = seleniumDriver.Capabilities.GetCapability("webSocketUrl") as string
?? throw new InvalidOperationException("Selenium did not return a webSocketUrl capability.");
BiDiDriver driver = new BiDiDriver();
await driver.StartAsync(webSocketUrl);
// The session already exists; do not call Session.NewSessionAsync.
// Stop the BiDiDriver before seleniumDriver.Quit() ends the session.
WebDriverBiDi.NET does not depend on Selenium; the two share only the session. Stop the BiDiDriver before calling Quit() on the Selenium driver, which ends the session.
Docker Container
Run the browser and its driver in the container and publish the driver's port; never publish a CDP port (--remote-debugging-address=0.0.0.0), which speaks CDP and exposes full browser control. The official Selenium images do this for you: selenium/standalone-chrome serves a WebDriver endpoint on port 4444 that accepts webSocketUrl: true and returns a BiDi URL routed through the container.
docker run -d -p 4444:4444 --shm-size=2g selenium/standalone-chrome:latest
Then create the session at http://localhost:4444 exactly as in the chromedriver steps above and connect to the returned webSocketUrl.
Common Launch Options
When the browser is launched by a driver, pass these as goog:chromeOptions.args / ms:edgeOptions.args / moz:firefoxOptions.args in the new-session capabilities; when you launch Firefox directly, put them on the command line.
Disable GPU
Useful for headless environments:
--disable-gpu
Window Size
Set initial window size:
--window-size=1920,1080
Disable Extensions
Start without extensions:
--disable-extensions
Incognito Mode
Start in incognito/private mode:
--incognito
No Sandbox (Docker/CI)
Disable sandboxing (needed in some containerized environments):
--no-sandbox
Disable Dev Shm (Docker)
Prevent shared memory issues in Docker:
--disable-dev-shm-usage
Example CI Launch
{
"capabilities": {
"alwaysMatch": {
"webSocketUrl": true,
"goog:chromeOptions": {
"args": ["--headless=new", "--disable-gpu", "--no-sandbox", "--disable-dev-shm-usage", "--window-size=1920,1080"]
}
}
}
}
Programmatic Browser Launch
You can start the driver executable yourself, create the session, and only then connect. The snippet below shows just the session and connection steps; the WebSocket Launcher Pattern further down shows launching the driver process as well:
// Start chromedriver (see the WebSocket Launcher Pattern), create a session, then connect
(string sessionId, string webSocketUrl) = await CreateBiDiSessionAsync("http://localhost:9515");
await driver.StartAsync(webSocketUrl);
Implementing Your Own Launcher
The WebDriverBiDi package provides only the protocol client. To launch browsers from code, use the Dramaturge.Browsers package, or write a launcher of your own; the patterns below sketch the two approaches.
WebSocket Launcher Pattern
Start the driver executable, wait for its /status endpoint, create a session with webSocketUrl: true, and connect to the returned URL. Ending the session closes the browser; then stop the driver process:
// Launch chromedriver (it launches Chrome itself when a session is created)
Process driverProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "chromedriver",
Arguments = "--port=9515",
UseShellExecute = false,
RedirectStandardOutput = true,
},
};
driverProcess.Start();
// Wait until the driver answers /status
using HttpClient client = new HttpClient();
while (true)
{
try
{
using HttpResponseMessage status = await client.GetAsync("http://localhost:9515/status");
if (status.IsSuccessStatusCode)
{
break;
}
}
catch (HttpRequestException)
{
// Not listening yet
}
await Task.Delay(100);
}
// Create the session and connect to its WebDriver BiDi endpoint
(string sessionId, string webSocketUrl) = await CreateBiDiSessionAsync("http://localhost:9515");
await driver.StartAsync(webSocketUrl);
// Later: clean up - end the session (closes the browser), then stop the driver process
await driver.Session.EndAsync();
await driver.StopAsync();
driverProcess.Kill();
Pipe Launcher Pattern
For pipe connections (Chromium only), implement IPipeServerProcessProvider to launch the browser with --remote-debugging-pipe and provide a Transport to BiDiDriver. Because the pipe carries CDP, that Transport must install a BiDi-over-CDP mapper — see ChromiumTransport in the Dramaturge.Browsers library. See the Transport and PipeConnection types in the API reference for the interface contract.
Troubleshooting
Port Already in Use
Error: Port 9515 already in use
Solutions:
- Stop the other driver instance
- Use a different port:
chromedriver --port=9516 - Find and kill the process using the port
StartAsync Cannot Connect
WebDriverBiDiTimeoutException: Could not connect to remote WebSocket server within 10 seconds
A connection that is refused, or fails for any other reason, is retried every 500 milliseconds until the
connection's StartupTimeout (10 seconds by default) runs out, so an endpoint that is not listening surfaces as this
timeout rather than as a refusal.
Solutions:
- Verify the driver (or Firefox with
--remote-debugging-port) is running - Check the WebSocket URL is correct
- Ensure no firewall is blocking the port
- Try
http://localhost:9515/statusin a browser to verify the driver is listening
Browser Closes Immediately
Solutions:
- Use
--user-data-dirto specify a profile - Check for conflicting flags
- Run without
--headlessto debug
Connected, but the First Command Fails
If StartAsync succeeds and the first command (or Session.StatusAsync) fails or times out, the URL is almost certainly a CDP endpoint:
Solutions:
- A URL containing
/devtools/browser/or/devtools/page/is Chrome's CDP endpoint; WebDriver BiDi is not spoken there - Use the
webSocketUrlfrom a driver's new-session response (ws://localhost:9515/session/<id>), or Firefox'sws://localhost:PORT/session
"session not created" After Connecting
Cause: Session.NewSessionAsync was called on a connection whose session already exists (any webSocketUrl returned by chromedriver, msedgedriver, geckodriver or Selenium).
Solution: Call NewSessionAsync only on the /session endpoints of Firefox or geckodriver, or after connecting through a mapper.
Best Practices
- Use a dedicated profile:
--user-data-dir(or let the driver create a temporary one) prevents conflicts - Port selection: Let the launcher pick a free port. Fix one only when something outside the test must know it in advance, and never share a fixed port between concurrent sessions
- Launch before connect: Wait for the driver's
/statusendpoint before creating a session - Clean shutdown: Close connections before killing browser
- Headless for CI: Use
--headless=newin CI environments - Log output: Redirect stdout/stderr when launching programmatically
Security Considerations
⚠️ Warning: A driver port, a Firefox remote-debugging port, and above all a Chrome CDP port expose full browser control. Do not:
- Run them on production systems
- Expose them to the internet
- Use with sensitive data without proper isolation
For production use:
- Run in isolated containers
- Use firewalls to restrict access
- Generate unique ports per session
- Clean up profiles after use
Next Steps
- Getting Started: Create your first application
- Your First Application: Complete tutorial
- Core Concepts: Understand the library
- Architecture: Deep dive into connection types
- Connection Management: Advanced connection scenarios