BrowserContext resource reclamation in high-volume test cycles
0 reputation · 21 Sept 2020, 16:36 UTC
0 reputation · 21 Sept 2020, 16:36 UTC
Playwright provides BrowserContext to isolate test environments without the overhead of restarting the entire browser process. In large-scale test suites where hundreds of contexts are created and destroyed sequentially, the goal is to ensure the browser process memory footprint remains stable.
While browserContext.close() is used to release session resources, there is uncertainty regarding the efficiency of memory reclamation for the underlying browser process after repeated cycles of context creation and destruction.
If pages are explicitly closed before the context is terminated, it is unclear if the browser process returns to its baseline resident set size (RSS) or if a gradual increase in heap usage persists over time.
BrowserContext once close() is called?28775 reputation · 22 Sept 2020, 00:29 UTC
Short answer: await context.close() releases everything Playwright owns for that context — pages, network handlers, storage state, cookies, and the corresponding browser-side incognito profile — but it does not guarantee the Chromium process's resident set size (RSS) returns to its exact pre-context baseline. And no, Playwright documents no fixed cycle-count threshold after which you must restart the browser.
Calling browserContext.close() is the reliable reclamation point. It tears down the context's pages, event listeners, routing, and storage, and tells Chromium to destroy the underlying profile. If you explicitly close pages first, that is fine but redundant — closing the context closes its pages. The key requirement is that every context is closed, ideally in a finally or afterEach hook so failures don't skip cleanup:
test.afterEach(async ({ context }) => {
await context.close();
});Keep one Browser for the whole suite and close it in afterAll. Restarting the browser per test is the expensive path contexts exist to avoid.
This is the likely explanation rather than a confirmed leak: even with perfect cleanup, the browser process's RSS typically does not return to its exact starting value. V8 and Blink allocators retain freed arenas for reuse, caches (font, shader, code) grow, and memory fragmentation accumulates. So a gradual, sub-linear increase over hundreds of cycles is expected behavior, not necessarily a leak. A true leak shows unbounded growth proportional to cycle count; allocator retention plateaus.
Common real leak sources in high-volume suites are not the context itself but what hangs off it: unresolved promises, downloads never awaited, tracing or video artifacts retained after failures, and contexts left open when a test throws before cleanup. Parallel workers should each close only the contexts they created, and concurrency should be bounded so simultaneous contexts stay within file-descriptor and memory limits.
There is no documented number of context cycles after which a browser restart is required. The practical approach is to measure and set your own policy. A simple verification loop:
const browser = await chromium.launch();
const baseline = process.memoryUsage().rss;
for (let i = 0; i < 500; i++) {
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto('about:blank');
await ctx.close();
}
global.gc?.(); // node --expose-gc
console.log(process.memoryUsage().rss - baseline);
await browser.close();Also watch the Chromium child processes' RSS via your OS tools — Node-side memory can look stable while the browser retains resources. If growth is unbounded across batches, restart the browser on a schedule (e.g., every N thousand contexts) as a pragmatic bound; if it plateaus, no restart is needed.
Exact cleanup timing and defaults are version-sensitive; confirm against the Playwright release your project pins. Closing a context while an async step still uses it causes flaky errors, so gate cleanup on test completion. If you reuse contexts instead of creating fresh ones, explicitly reset cookies, storage, and permissions — otherwise state leaks between tests even when memory does not.
Use comments to ask for clarification. Post a solution as an answer.
28,775 reputation · 22 Sept 2020, 02:09 UTC
To build on the discussion of RSS drift, it is important to distinguish between the memory managed by the browser process and the memory managed by the test runner's language wrapper (e.g., Node.js or Python). While await context.close() signals the browser to destroy the incognito profile and associated pages, the JavaScript or Python object representing that context remains in the runner's heap until garbage collected.
In extremely high-volume cycles, if the test runner's GC does not trigger frequently enough, you may see a secondary memory climb in the runner process, even if the browser process is stable. To verify if this is occurring, you can monitor the resident set size (RSS) of both the browser and the test runner separately using system tools like htop or Activity Monitor. If the runner's memory grows while the browser's plateaus, the issue is likely object retention in the test suite rather than a browser-side leak.