How to Find and Fix Memory Leaks in Browser Automation
Learn to distinguish leaks from normal memory use, find what is retaining objects, and verify fixes across repeatable browser automation runs.
A single high memory reading does not prove a leak. To find one, repeat the same automation workflow, measure the same processes at equivalent points, and compare heap snapshots to see which objects remain reachable and what retains them. Then fix the owner’s cleanup and repeat the workload to check whether growth stops.
Browser page JavaScript, the Node.js automation runner, browser subprocesses, and whole-process RSS are different memory domains. Diagnose the one that is growing; no single metric describes them all.
1. Reproduce the growth
- Choose one test or automation cycle that reliably shows growth.
- Keep the browser and automation-library versions, input data volume, and worker count stable.
- Record memory at the same points each time: after setup, after the repeated action, and after teardown.
- Run several cycles, allow the normal cleanup and settling period, then compare readings or snapshots.
Separate peak memory during an action from memory that remains after cleanup. A temporary allocation, normal cache growth, or a single transient spike is not sufficient evidence of a leak. A stronger signal is a repeatable increase in objects that should have been released, together with a retaining path that explains why they remain.
2. Identify the process and memory domain
First determine what your monitor is measuring. A browser page’s JavaScript heap, the Node.js runner’s V8 heap, browser-native or subprocess memory, and process RSS can move differently.
| Observation | Where to investigate |
|---|---|
| Page JavaScript heap grows after the same page action | Chrome DevTools Memory tools and page heap snapshots |
| Node.js heap grows across automation cycles | V8 heap statistics and snapshots for the runner isolate |
| RSS grows while the Node.js V8 heap stays comparatively stable | Native allocations, browser subprocesses, or other process memory; RSS is broader than the V8 heap |
| Growth appears only at peak concurrency | Repeat at stable worker counts and compare per-cycle and peak observations |
Do not label RSS growth a JavaScript object leak without heap evidence. Node’s process.memoryUsage() reports RSS as well as values such as heap usage and external memory; V8 heap statistics describe the V8 heap, not every process involved. See the [Node.js V8 documentation](https://nodejs.org/api/v8.html).
3. Inspect page JavaScript with Chrome DevTools
- Open the page in Chrome DevTools and use the Memory panel to capture a baseline heap snapshot.
- Run the same page action or automation cycle several times.
- Allow the normal cleanup and settling period, then capture another snapshot under equivalent conditions.
- Use the Comparison view to find objects that persist or accumulate. Inspect object counts, freed memory, and retaining paths.
- Follow references to their owners. Look for detached DOM nodes retained by JavaScript, closures, globals, collections, or event handlers.
Heap snapshots describe reachable JavaScript objects at a point in time. Chrome’s snapshot capture begins with garbage collection, but snapshots do not describe every native allocation. A detached DOM node cannot be collected while JavaScript still holds a reference to it. Follow the retaining path to find which object or handler still owns it. See Chrome’s guides to [recording heap snapshots](https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots/) and [fixing memory problems](https://developer.chrome.com/docs/devtools/memory-problems?authuser=90).
4. Measure a Node.js automation runner
For a Node.js runner, sample both process memory and V8 heap statistics at consistent lifecycle points. This runnable example records a snapshot before and after a workload and can optionally write V8 statistics as JSON. Run it as a controlled diagnostic, not as harmless production instrumentation.
// memory-check.mjs
import { writeHeapSnapshot, getHeapStatistics } from 'node:v8';
import { memoryUsage } from 'node:process';
function report(label) {
const processMemory = memoryUsage();
const v8 = getHeapStatistics();
console.log(JSON.stringify({
label,
rss: processMemory.rss,
heapUsed: processMemory.heapUsed,
heapTotal: processMemory.heapTotal,
external: processMemory.external,
arrayBuffers: processMemory.arrayBuffers,
v8UsedHeapSize: v8.used_heap_size,
v8TotalHeapSize: v8.total_heap_size,
v8ExternalMemory: v8.external_memory,
}));
}
report('before');
// Replace with one repeatable automation cycle, and await its teardown.
await runOneAutomationCycle();
report('after-one-cycle');
// Capture only when there is sufficient memory headroom. This blocks the event loop.
const snapshotPath = writeHeapSnapshot();
console.log(`Heap snapshot written to ${snapshotPath}`);
async function runOneAutomationCycle() {
// Example placeholder: invoke the same test or task you are investigating.
}
The placeholder must be replaced by the actual repeatable workload for the comparison to be useful. Capture again after several identical cycles and teardown. For a more focused comparison, use the same checkpoints and inspect snapshots in Chrome DevTools. Node’s [V8 API](https://nodejs.org/api/v8.html) documents getHeapStatistics() and writeHeapSnapshot().
Interpret the measurements
heapUsedand V8used_heap_sizedescribe live and currently used V8 heap space; compare equivalent checkpoints rather than reacting to a peak.total_heap_sizeis allocated V8 heap capacity and need not track live objects directly.externalandexternal_memorycover memory associated with native objects and managed by V8 accounting; they are not a complete accounting of browser processes.rssis resident memory for the Node.js process, broader than its V8 heap. If RSS rises but heap measurements do not, investigate outside the JavaScript object graph too.
A Node heap snapshot belongs to one V8 isolate. If your runner uses worker threads, each worker has its own isolate and requires its own capture. A runner snapshot will not explain memory held by a separate browser process.
5. Check likely owners in automation code
Use the retaining path or the measured lifecycle to identify a specific owner before changing code. Common hypotheses to check include:
- Pages or contexts outlive their work. Check that pages and explicitly created browser contexts are closed when their owner finishes.
- Listeners accumulate. Check whether each iteration adds page or browser event handlers without removing them when no longer needed.
- References keep page objects alive. Inspect globals, closures, arrays, maps, and test fixtures that may retain pages, elements, or detached DOM nodes.
- Workload data accumulates. Check whether logs, response bodies, screenshots, traces, or application caches are retained across iterations without an intended bound.
- Cleanup is skipped on failure. Ensure teardown runs when a test throws, times out, or is cancelled.
These are investigation paths, not a claim that every memory issue has one of these causes. Playwright documents [page event listener APIs](https://playwright.dev/docs/api/class-page) and [browser context closure](https://playwright.dev/docs/next/api/class-browser). Its browser guidance describes explicitly closing contexts before the browser when graceful page closure and close events matter.
6. Fix ownership and verify the result
- Identify the retaining reference or lifecycle owner from measurements and snapshots.
- Release the reference when its owner is finished. Remove listeners, discard obsolete collections, and close pages or explicitly created contexts at teardown.
- Put cleanup on the failure path too, typically in the test fixture or a
finallyblock. - Preserve intentional caches only when their lifetime and size are understood and bounded.
- Repeat the original workload with the same versions, data, worker count, checkpoints, and settling period.
- Compare the same evidence again. The fix is supported when the suspected retained-object growth stops and the relevant process’s trend stabilizes under those conditions.
Increasing a memory limit can postpone a failure, but it does not change what retains objects. Do not call a code change a fix based on one run or an unmeasured impression.
7. Troubleshoot common symptoms
| Symptom | Likely explanation | Next step |
|---|---|---|
| Memory rises during a run, then falls after teardown | Temporary allocation or expected workload peak | Compare after equivalent cleanup points across repeated cycles; inspect retained objects before calling it a leak. |
| Page heap grows across cycles | Page objects remain reachable, potentially through detached DOM nodes or handlers | Compare page snapshots and follow retaining paths to the owning reference. |
Runner heapUsed grows after each cycle |
Runner objects may remain referenced, or cleanup may be incomplete | Take equivalent Node snapshots and inspect surviving object types and references. |
| RSS grows but the Node heap is steady | RSS includes memory beyond the V8 heap; browser subprocesses or native memory may be involved | Measure the browser and runner processes separately and investigate the non-heap domain. |
| Snapshot capture blocks a test or service | Node snapshot creation is synchronous and blocks the event loop | Capture in a controlled diagnostic window rather than on a latency-sensitive path. |
| Snapshot capture runs out of memory | Snapshot creation needs substantial additional headroom | Capture on a machine with enough memory, reduce competing workload, and avoid capturing under tight limits. |
| Worker-thread leak is absent from the main snapshot | Each worker has a separate V8 isolate | Capture and compare the worker isolate that owns the workload. |
| Growth appears only with more workers | Higher concurrency may increase live working data or expose lifecycle behavior | Hold worker count stable while diagnosing, then compare counts deliberately. |
8. Performance, reliability, and diagnostic cost
- Keep the experiment controlled. Changing browser version, library version, data volume, or concurrency between measurements makes comparisons harder to interpret.
- Use snapshots selectively. They are useful for object retention and retaining paths, but capture has runtime and memory costs.
- Plan Node snapshot headroom. Snapshot creation is synchronous, blocks the event loop, applies to one isolate, and needs memory about twice the heap size at capture. On constrained machines it can terminate with an out-of-memory condition. Avoid treating it as a free production action.
- Separate scopes. A page snapshot, a Node isolate snapshot, and process RSS answer different questions. Record which process and metric each observation represents.
- Verify under the same workload. A stable result in one run is weaker evidence than a stable trend across repeated equivalent cycles.
Chrome DevTools is suited to inspecting page JavaScript reachability. Node snapshots help inspect the runner’s V8 isolate. Neither alone accounts for all browser-native memory or every subprocess.
Or skip the browser setup
If your task is to capture a page while diagnosing a workflow, ScreenshotNeo provides a one-request screenshot API and an MCP server. The API returns an image or PDF; it does not replace heap profiling or measure automation memory. See the ScreenshotNeo website and API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners, popups, and chat widgets are removed before the shot.
- Bot checks, blank pages, and failed loads are never billed.
- An MCP server lets AI agents take screenshots.
- 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.
Sign up free for 1,000 screenshots a month, with no card.
FAQ
Does a high RSS value prove a JavaScript leak?
No. RSS includes memory beyond the V8 heap. Compare heap measurements and inspect the relevant processes separately.
Why can a detached DOM node remain in a snapshot?
JavaScript can keep it reachable through a reference such as a closure, global, collection, or event handler. Follow its retaining path to the owner.
Can I take one Node.js snapshot for all workers?
No. A snapshot captures one V8 isolate. Capture the worker isolate separately when it owns the workload.
Does a stable heap capacity mean the leak is fixed?
Not by itself. Compare retained objects and equivalent post-cleanup measurements across repeated cycles; allocated heap capacity is not the same as live retained objects.


