How does a k6 script drive a Chromium browser that k6 did not launch itself?
answer
- a second entry point in the module
- you supply the browser, not k6
- no browser type option needed
- connectOverCDP with a ws endpoint
basics
~10 sImport chromium from k6/browser and await chromium.connectOverCDP(wsEndpoint) with the browser's DevTools WebSocket URL. That scenario needs no options.browser block, because k6 launches nothing, and the browser it returns does expose close().
solid answer
~40 s`k6/browser` exports a second entry point beside `browser`: the `chromium` browser type. `await chromium.connectOverCDP(wsEndpoint)` attaches to an already running Chromium over the DevTools protocol and returns a Browser you use exactly like the managed one — `newPage()`, `newContext()` and the rest. Two things differ. First, the scenario needs no `options.browser` block, because there is no browser for k6 to launch and therefore no type to resolve; the `K6_BROWSER_*` variables that apply to an existing browser, such as the timeout and debug settings, still apply. Second, this browser **does** expose `close()`, since you own it. Closing releases k6's connection early; the browser itself keeps running.
code
javascript · 18 linesimport http from 'k6/http';
import { chromium } from 'k6/browser';
export function setup() {
const res = http.post('https://provider.example/v1/sessions');
return { wsURL: res.json().connectUrl };
}
export default async function (data) {
const browser = await chromium.connectOverCDP(data.wsURL);
const page = await browser.newPage();
try {
await page.goto('https://quickpizza.grafana.com/');
} finally {
await page.close();
await browser.close();
}
}go deeper
Just recognise that k6/browser exports a chromium property as well as browser, and that it is the way to attach to a Chromium that is already running somewhere.
Explain the two differences from the managed path: no browser type option is needed, and the returned browser does have close(). Both follow from k6 not owning the process.
Show the operational shape: fetch the session endpoint once in setup, connect inside each iteration, close early when it helps, and check isConnected when attached to something you did not start.
Weigh the ownership trade. Attaching to browsers you provision takes them out of k6's hands but makes you responsible for their supply, cleanup and version drift — decide whether that is a platform your team wants to own.
## Two ways into a browser The `k6/browser` module offers two entry points, and they differ in who owns the browser process. | | `browser` (managed) | `chromium.connectOverCDP()` | |---|---|---| | Who starts Chromium | k6, at iteration start | you, or a provider, outside the run | | Needs `options.browser.type` | yes | **no** | | Exposes `close()` | no | yes | | Lifetime of the process | one iteration | whatever you gave it | | Endpoint | none needed | a `ws://` or `wss://` DevTools URL | Both return the same Browser shape, so the page-level half of a script is identical either way. ## Making the connection ```javascript import { chromium } from 'k6/browser'; export default async function () { const browser = await chromium.connectOverCDP(__ENV.CDP_WS_URL); const page = await browser.newPage(); try { await page.goto('https://quickpizza.grafana.com/'); } finally { await page.close(); await browser.close(); } } ``` `connectOverCDP` takes one required argument, the WebSocket endpoint of the browser's DevTools interface, which must use the `ws` or `wss` scheme — for a locally started Chromium that is something like `ws://localhost:9222/devtools/browser/<id>`, read from the `webSocketDebuggerUrl` field of `http://localhost:9222/json/version`. It returns a promise, so it is awaited like everything else in the k6 v2 browser API. Because the call happens **inside** the iteration, the endpoint can be computed at run time. The common shape is to obtain a session from a browser provider once in `setup()`, return the URL from it, and let every iteration connect using the value handed to the entry function. ## What the scenario no longer needs The most surprising part is the configuration that disappears. A `connectOverCDP` scenario does not set `options.browser.type`: - there is no browser for k6 to launch, so there is no browser type to resolve; - the module builds this browser lazily, at the moment you call `connectOverCDP`, rather than eagerly at iteration start; - iterations in such a scenario are not browser iterations from k6's point of view, and the module still sweeps up the connections they created when the iteration ends. The environment variables that describe an existing browser rather than a launch — the module's global timeout and its debug logging — are still honoured. The ones that describe a launch, such as the headless flag, the extra Chrome arguments and the executable path, have nothing to act on. ## Who owns the browser afterwards This is the ownership inversion worth being explicit about. On the managed browser, k6 launches and k6 destroys, and there is no `close()` for you to call. On a connected browser: 1. **k6 closes the connection at iteration end anyway**, exactly as it does for a browser it launched — so calling `close()` is optional. 2. **Calling `close()` yourself releases the connection early**, which is what you want if the iteration has more work to do afterwards. 3. **Closing the connection does not stop the browser.** You own the process, so it keeps running and stays available to later iterations, or to whatever else is using it. 4. **Closing twice is a no-op**, but using the browser after closing it throws — treat `close()` as the last thing you do with the object. `browser.isConnected()` reports whether the CDP connection is still live, which is the natural check when you are attached to something you did not start. ## What stays the same Everything downstream of the connection is unchanged, which is the point of the design: - The Browser it returns has the same methods, so `newPage()`, `newContext()` and `context()` behave identically. - Pages are still yours to close, and the same `try`/`finally` shape applies. - The run still emits the `browser_*` metric family, including the web vitals, for the pages you drive. - Every call is still promise-based, so the entry function is still `async`. ## When it earns its place The managed browser is the default and covers most scripts. Connecting over CDP is the answer to a narrow set of situations: - the browsers run somewhere other than the load generator — a browser grid or a hosted browser provider that issues a session URL per test; - the browser needs a configuration k6's launch path does not offer, such as a specific profile, an extension, or flags set by whatever started it; - the same browser instance must outlive a single iteration, because something outside k6 is managing it. In exchange you take on provisioning, cleanup and version-matching of the browser yourself, which is exactly the work the managed path exists to avoid.
- Why does a connectOverCDP scenario not need options.browser.type?Because that option tells k6 which browser to launch, and here k6 launches nothing. There is no type to resolve, so the scenario is configured like any ordinary one and the browser is built when `connectOverCDP` is called inside the iteration.
- What actually happens when you call close() on a connected browser?k6 releases its CDP connection, along with the contexts and pages on it. The browser process itself is untouched and keeps running, because you own it. k6 would have released the connection at iteration end anyway; calling close() just does it sooner.
saying these in an interview costs you the question
- Adds options.browser.type to a connectOverCDP scenario
- Thinks browser.close() also stops the remote browser process
- Passes an http:// endpoint instead of a ws:// one
- Expects K6_BROWSER_HEADLESS to affect a browser k6 did not launch
- Calls connectOverCDP in init context rather than inside the iteration