In a k6 browser scenario, what closes the Chromium process, and why still call page.close()?
answer
- scoped to something smaller than the run
- launched and destroyed per iteration
- no close on the managed browser
- your job is the page, in finally
basics
~20 sk6 owns the browser: it launches one when an iteration starts and tears it down when the iteration ends, and the managed browser exposes no close() at all. Pages are yours — close each one, normally in a finally block.
solid answer
~40 sThe browser module subscribes to k6's own iteration events. On iteration start it launches a Chromium for that VU's iteration and registers it; on iteration end it destroys it. That is why the `browser` object you import is not a global handle but a per-iteration lookup, and why calling it outside a browser-enabled iteration fails with `browser not found in registry`. It is also why the k6-managed browser deliberately has **no** `close()` method — only a browser you attached yourself over CDP gets one. Pages are the part you do own: `await page.close()`, normally in a `finally`, so it runs even when an assertion throws. Leaving pages open can distort web-vital metrics and leaves event handlers registered.
code
javascript · 24 linesimport { browser } from 'k6/browser';
export const options = {
scenarios: {
ui: {
executor: 'per-vu-iterations',
vus: 2,
iterations: 5,
options: {
browser: { type: 'chromium' },
},
},
},
};
// ten iterations, ten browsers launched and torn down by k6
export default async function () {
const page = await browser.newPage();
try {
await page.goto('https://quickpizza.grafana.com/');
} finally {
await page.close();
}
}go deeper
Remember the shape: open the page, work inside a try, and close the page in a finally. You never close the browser yourself in a k6 browser scenario.
Explain the iteration scoping — a browser is launched on iteration start and destroyed on iteration end — and that the managed browser has no close() because k6 owns it.
Reason from it: why sessions cannot persist across iterations, why an unclosed page shows up as distorted web vitals rather than as an error, and why the close needs its own await inside the finally.
A browser per iteration is the unit k6 charges you for, so decide how your team maps a user journey onto iterations — one long iteration or several short ones — and what that forces about setup data and login.
## The browser is scoped to the iteration A k6 browser scenario does not start one Chromium for the test, or one per VU for the whole run. The browser module subscribes to k6's iteration lifecycle events and reacts to two of them: 1. **Iteration start** — if this scenario has a browser block, the module launches a Chromium process (or connects to a remote one, if a WebSocket URL was configured) and stores it in a registry keyed by the iteration. 2. **Iteration end** — the module removes that entry and shuts the browser down. The registry lookup is what makes everything else make sense. The `browser` you import is not a long-lived object; it is a thin façade that resolves the current iteration's browser every time you touch it. A VU running its 40th iteration in a browser scenario has launched and torn down 40 browsers. ## Why the failure message says "registry" When a scenario has no browser block, nothing is ever put in that registry for its iterations. The first `browser.newPage()` then fails with **`browser not found in registry. make sure to set browser type option in scenario definition in order to use the browser module`** — an error that reads oddly until you know the lookup exists. It is not saying the browser crashed; it is saying no browser was ever created for this iteration, because this scenario never asked for one. ## There is no `browser.close()` This is the part that catches people arriving from other automation tools, where closing the browser is the last line of every script. On a k6-managed browser the method is simply not there — the module attaches `close()` only to a browser the script created itself via `chromium.connectOverCDP()`. Calling `browser.close()` on the managed one is a type error about a method that does not exist. | Object | Who closes it | Has `close()`? | |---|---|---| | The k6-managed `browser` | k6, at iteration end | No | | A browser from `chromium.connectOverCDP()` | k6 at iteration end, or you, earlier | Yes | | A `BrowserContext` | you, or k6 with the browser | Yes | | A `Page` | **you** | Yes | ## Why `page.close()` still matters If k6 destroys the browser at the end of the iteration anyway, closing a page can look like a formality. It is not, for reasons that are specific to how the module reports: - **Web vitals are finalised around the page.** A page left open when the iteration ends can distort the web-vital metrics for the run, because the measurements are not flushed cleanly. - **Event handlers registered on the page** — anything attached through the page's event API — are cleaned up on close; skipping it leaks them for the rest of the iteration. - **Metric samples are flushed** when the page closes, so an unclosed page can leave incomplete browser metrics behind. The idiom the module's own examples use, and the one to write by reflex, puts the close in a `finally` so it survives a thrown assertion: ```javascript export default async function () { const page = await browser.newPage(); try { await page.goto('https://quickpizza.grafana.com/'); // interact, read values, assert } finally { await page.close(); } } ``` Note the `await` on the close itself. `page.close()` returns a promise like almost everything else in k6 v2's browser API, so an unawaited close may not have finished by the time the iteration ends. ## The `browser` object is not what it looks like Written at module scope, `import { browser } from 'k6/browser'` looks like it binds a single browser for the life of the script. It does not. Each property access resolves the browser registered for the iteration currently executing in that VU, which explains several behaviours that otherwise look arbitrary: - It cannot be used in the init context, where no iteration and therefore no browser exists. - The same imported name refers to a different Chromium in every iteration. - A scenario without a browser block never populates the registry, so every access fails the same way. - Two scenarios that both enable the browser get independent browsers; nothing is shared between them. ## What this means when things go wrong Two diagnostic habits follow from the iteration scoping: 1. **A leaked browser process is a k6 bug, not yours** — the module kills the processes it launched, and it registers their PIDs precisely so it can. What you can leak is pages within an iteration. 2. **State does not survive an iteration.** Cookies, storage and any logged-in session live in a browser context inside a browser that is destroyed when the iteration ends, so each iteration starts from a cold browser. Anything you need to carry across iterations has to be re-established inside the iteration or passed in as data.
- Why does the k6-managed browser deliberately lack a close() method?Because k6 owns its lifecycle. The module launches it on iteration start and shuts it down on iteration end, so a script-driven close would race that teardown. Only a browser you attached yourself through `chromium.connectOverCDP()` exposes `close()`, since you own that one.
- Can a browser scenario reuse a logged-in session across iterations?Not through the browser itself — the browser and its contexts are destroyed at iteration end, so every iteration starts cold. Carry the credentials or a token as data and re-establish the session inside the iteration, or drive the login once per iteration deliberately.
- What breaks if page.close() is left out?Nothing crashes, which is why it survives review. Web-vital measurements for that page can be distorted, page event handlers stay registered, and metric samples may not be flushed cleanly — so the run finishes with quietly incomplete browser numbers.
The managed browser is a hire car booked for a single trip: k6 collects it when the iteration starts and returns it when the iteration ends, which is why your script is never handed the keys to scrap it. The pages you open inside it are yours to tidy up before you hand it back.
saying these in an interview costs you the question
- Calls browser.close() on the k6-managed browser
- Assumes one Chromium is shared by the whole test run
- Expects cookies or a login to persist across iterations
- Puts page.close() after the assertions instead of in finally
- Says page.close() is optional because k6 tears the browser down anyway
- Forgets to await page.close() inside the finally block