Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Launch or connect to one Puppeteer Browser outside your request handler, create a new Page (and, when needed, an isolated BrowserContext) for each job, then close that task’s page in a finally block. Keep the browser alive between jobs and close it once during application shutdown. This removes repeated Chrome startup work without allowing cookies, navigation state, or event handlers to leak between tasks.

The reusable lifecycle

A reliable service has three lifetimes:

  • Browser lifetime: one process-wide instance, launched or connected during initialization.
  • Job lifetime: a page, or a context plus page, leased to one render or automation task.
  • Application lifetime: graceful browser shutdown when your service exits.

Puppeteer’s Page API notes that one Browser instance can have multiple Page instances. browser.newPage() creates a page in the default browser context and returns a promise, so it is the normal way to obtain a fresh tab for each independent operation.

Minimal reusable module

import puppeteer from 'puppeteer';

// Launch once, when the service starts.
const browser = await puppeteer.launch();

export async function render(url) {
  const page = await browser.newPage();
  try {
    await page.goto(url, { waitUntil: 'networkidle2' });
    return await page.content();
  } finally {
    await page.close();
  }
}

// Call this from your application's shutdown hook.
export async function shutdown() {
  await browser.close();
}

Do not put puppeteer.launch() inside render() or an HTTP route. Every request would create another Chrome process, pay startup overhead, consume more memory, and make cleanup harder. Conversely, do not keep reusing one page for unrelated callers: a second navigation can replace the first caller’s URL and DOM while its work is still running.

Close versus disconnect: who owns Chrome?

Use browser.close() for an owned browser

If this process called puppeteer.launch(), it owns the browser. Call await browser.close() during service shutdown. This gracefully terminates Chrome and its pages. Avoid closing it after every request; that defeats reuse.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use browser.disconnect() for an externally managed browser

If you connected with puppeteer.connect() to a browser managed by another process or service, disconnect when your work is complete. browser.disconnect() detaches your Puppeteer client without shutting down Chrome or closing its pages. Only the owner should close that browser.

Reconnect with a retained WebSocket endpoint

import puppeteer from 'puppeteer';

export async function renderOnRemoteBrowser(browserWSEndpoint, url) {
  const browser = await puppeteer.connect({ browserWSEndpoint });
  const page = await browser.newPage();
  try {
    await page.goto(url, { waitUntil: 'domcontentloaded' });
    return await page.title();
  } finally {
    await page.close();
    // Leave the remotely owned browser running.
    browser.disconnect();
  }
}

Reconnect only while the endpoint is valid and the remote browser is still alive. A disconnect event should be treated as a signal to stop accepting work, recreate or reconnect the browser, and then resume.

Pages, contexts, and state isolation

When a new page is enough

Use browser.newPage() when jobs may share the default context intentionally. Each page has its own URL, viewport, navigation history, and page-scoped handlers, but cookies and local storage in the same context can persist between pages. This is useful for a deliberate shared login session or a small workflow whose steps belong to the same user.

When to create a BrowserContext

A BrowserContext is the isolation boundary for cookies and local storage. Create one per user, tenant, or untrusted job when state must not leak. The context API’s context.close() closes the context and every page inside it; the default browser context cannot be closed.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
export async function isolatedRender(browser, url) {
  const context = await browser.createBrowserContext();
  try {
    const page = await context.newPage();
    await page.goto(url, { waitUntil: 'networkidle2' });
    return await page.content();
  } finally {
    // Closes the page(s) created in this temporary context.
    await context.close();
  }
}

Do not create a context merely because it sounds safer. Context creation adds lifecycle work; use it where separate cookies, storage, permissions, or identities are a requirement. Reusing one context is correct only when shared state is intentional and controlled.

Concurrency: give every job an exclusive lease

Treat a page as exclusively owned by one workflow until that workflow finishes. Two callers navigating the same page can overwrite the URL, DOM, cookies, dialogs, request interception, viewport, and event listeners. The safe default is one fresh page per concurrent job, with a fresh context as well when session isolation is required.

A bounded page pool

For bursty traffic, keep a bounded number of browser or page leases and queue requests when all leases are busy. A pool should:

  • hand a page to exactly one job at a time;
  • reset or retire a page after a failed or cancelled workflow;
  • close every page on release, unless you have a measured, carefully reset pooling strategy;
  • stop admitting work when the browser disconnects; and
  • record queue wait, navigation duration, failures, and browser/page memory growth.

Puppeteer’s official references do not define a universal page-count or memory limit. Capacity depends on the target sites, JavaScript, media, viewport, and concurrency. Measure your workload rather than copying a page number from another deployment.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Simple semaphore pattern

class PageLimiter {
  constructor(limit) {
    this.limit = limit;
    this.active = 0;
    this.waiters = [];
  }
  async acquire() {
    if (this.active < this.limit) {
      this.active++;
      return;
    }
    await new Promise(resolve => this.waiters.push(resolve));
    this.active++;
  }
  release() {
    this.active--;
    this.waiters.shift()?.();
  }
}

const limiter = new PageLimiter(8);

export async function queuedRender(browser, url) {
  await limiter.acquire();
  const page = await browser.newPage();
  try {
    await page.goto(url, { waitUntil: 'networkidle2', timeout: 45_000 });
    return await page.content();
  } finally {
    await page.close();
    limiter.release();
  }
}

The limit of eight is only an example, not a recommended capacity. Tune it from measurements and the memory headroom of your host.

Cleanup that survives failures

Always put page cleanup in finally, so timeouts, thrown assertions, navigation errors, and cancelled work cannot strand tabs. If you use temporary contexts, close the context in finally; that closes its pages together. Remove listeners and abort pending work when retiring a page, especially if you installed request interception, dialog handlers, or long-lived timers.

  • Close every page created for a task.
  • Close temporary contexts after their workflow.
  • Keep browser shutdown in one application-level hook.
  • Close an owned browser; disconnect from an externally owned browser.
  • On disconnect, recreate or reconnect before accepting new jobs.

Request-driven service structure

  1. Initialize: launch Chrome or connect to a managed endpoint before the server begins accepting requests.
  2. Lease: create a page for each request; create a context first when isolation is required.
  3. Configure: apply per-job headers, cookies, viewport, user agent, and timeouts to that lease only.
  4. Navigate and work: perform the task, waiting for the condition that actually means the page is ready. networkidle2 is not appropriate for every site, particularly pages with continuous polling.
  5. Release: close the page or context in finally, then return the result.
  6. Shutdown: stop intake, let active jobs finish or cancel them, and close the owned browser once.

Performance, reliability, and cost trade-offs

What reuse improves

Keeping Chrome warm removes repeated process startup and browser initialization. It also allows multiple jobs to run through one managed browser rather than spawning an unbounded process per request.

What reuse does not solve

A warm browser does not make pages safe to share concurrently, does not isolate cookies, and does not prevent a target site from timing out or blocking automation. Each page still needs navigation timeouts, cancellation behavior, and deterministic release.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When to retire a page or browser

Retire a page after unrecoverable protocol errors, stuck request interception, or state you cannot confidently reset. Recreate the browser after a browser-level disconnect or corruption. Keep metrics for active pages, queue depth, navigation time, error classes, and memory so retirement and pool sizing are evidence-based.

Common failures and fixes

Chrome launches for every request

Cause: puppeteer.launch() is inside the handler. Fix: move launch to initialization and pass the shared browser to the render function.

One request sees another request’s page

Cause: callers share a page. Fix: create one page per concurrent workflow and never expose that page to another caller before cleanup.

Users appear logged in as one another

Cause: cookies or local storage are shared in one context. Fix: create a separate BrowserContext per user or job and close it afterward.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

browser.close() breaks other clients

Cause: a connected client closed a browser it did not own. Fix: call browser.disconnect(); let the process that launched Chrome decide when to close it.

Pages accumulate after errors

Cause: cleanup occurs only on the success path. Fix: put page.close() or context.close() in finally, including timeout and cancellation paths.

Requests hang at network idle

Cause: analytics, websockets, or polling keep network activity alive. Fix: choose a more suitable readiness condition, wait for a specific selector, and enforce a finite timeout.

Reconnect fails

Cause: the remote endpoint expired, the browser exited, or the endpoint is unreachable. Fix: mark the browser unavailable, recreate or obtain a fresh endpoint, and only then resume queued work. Do not blindly retry against a dead connection.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is dependable website screenshots rather than managing Chrome yourself, ScreenshotNeo provides a website screenshot API and MCP server. It accepts a URL and returns PNG, JPEG, WebP, or PDF. Before capture it can accept cookie or consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and whether the shot was billed.

One request is enough:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for options such as full-page lazy-image capture, CSS-selector element shots, dark mode, device presets, retina scale, PDF paper settings and page ranges, custom CSS or JavaScript, click-before-capture, wait conditions, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage data, and the OpenAPI specification. Its parameter names also accept the names used by other screenshot APIs, which can simplify migration.

Python

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)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

An MCP server supplies take_screenshot, get_page_info, and capture_pdf tools to Claude, Cursor, and other MCP clients. Every feature is available on every plan: 1,000 shots per month are free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Frequently Asked Questions

Should I reuse a Page object between sequential jobs?

Usually no. Create and close a page per job unless you have a measured reason to pool pages and a complete reset procedure for every page-scoped setting and listener.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I close the default BrowserContext?

No. The default context cannot be closed; use a separately created context when you need a context that can be closed with all its pages.

Does a warm browser guarantee faster screenshots?

It removes browser-launch overhead, but navigation, JavaScript execution, network conditions, and target-site behavior still determine each job’s duration.

How many pages can one Puppeteer browser handle?

There is no universal official limit. Determine a safe bound with measurements from your sites, workload, concurrency, and available memory.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.