The Browser Is a System of Targets, Contexts, and Sessions

Puppeteer’s current problem is not how to open a tab; it is how to keep ownership and state intelligible after a Node script connects to a live browser. This chapter follows that path from launch or connection through protocol sessions, browser contexts, pages, frames, workers, and targets. The payoff is practical: when a run fails or needs cleanup, readers can identify which object owns the relevant state instead of changing an unrelated selector or killing the wrong process.

The public API makes the first step look simple. A script launches Puppeteer, creates a page, navigates, performs work, and closes the browser. Behind that sequence, however, the library must establish a transport, select a protocol implementation, discover browser entities, and keep its object model synchronized as those entities appear and disappear. The hierarchy is the explanation for the API.

Launching and connecting are different kinds of ownership

Launching means Puppeteer starts a browser process through its Node-specific launcher. In the standard puppeteer package, that usually includes a managed browser choice: the package downloads a compatible Chrome for Testing build and uses it with Puppeteer’s core library. The caller can configure the browser, select Firefox, choose a channel, or provide an executable path, but each choice changes who owns compatibility and process setup.

Connecting begins from the opposite assumption. The browser already exists, perhaps because another service started it, a test runner owns it, or a separate process exposes a debugging endpoint. puppeteer.connect() accepts one connection source at a time: a WebSocket endpoint, a browser URL, a custom transport, or a browser channel in the Node environment. When given a browser URL, Puppeteer discovers the browser’s WebSocket endpoint and then opens the transport. It is not launching a second browser; it is attaching a control client to an existing one.

That distinction determines what a live Browser object means. A browser returned from launch() is associated with a child process that Puppeteer can close. A browser returned from connect() may have no such process. Both expose pages and targets, but they do not have the same authority over the underlying resource. The API therefore separates browser.close() from browser.disconnect(): closing shuts down the browser and its associated pages, while disconnecting removes Puppeteer’s connection and leaves the browser process running.

This is an operational boundary, not a naming detail. A remote browser service normally wants a client to disconnect without terminating a shared browser. A one-off local script usually wants a finally path that closes what it launched. If code treats every browser object as process-owned, a reconnectable session becomes fragile. If it treats every browser object as remote, local browser processes can outlive the test that created them.

The connection itself also has structure. Puppeteer sends commands through a transport and correlates responses with pending calls. CDP connections can create sessions for individual targets, forward events, and reject pending calls when the connection closes. This is why a protocol error is not the same thing as a page assertion failure: one concerns the control channel, the other concerns what the page did.

The selected protocol is made at this boundary. Puppeteer’s common connector validates the connection source, creates the transport, and dispatches to a CDP or WebDriver BiDi implementation. When a browser URL is supplied, the connector first discovers the browser’s WebSocket endpoint through the browser’s version endpoint; it does not guess among several possible control channels. The public Browser object is therefore a stable-looking façade over a particular session strategy. Later chapters will examine the differences between those strategies; for now, the important point is that a connection is the root of the object graph.

That root also carries the control channel’s timing and failure semantics. A CDP connection assigns identifiers to commands, keeps callbacks for responses, and creates sessions for individual targets. Events can arrive independently of the command that caused them, so a page becoming available, a frame detaching, and a browser disconnecting are not interchangeable outcomes. When the transport closes, pending protocol calls are cleared and the associated sessions are closed. A rejected command at this layer says that the control conversation failed; it does not by itself say that the website returned the wrong content.

There is a security implication in the shape of the interface. A WebSocket endpoint is a route to browser control, not a public page URL. Puppeteer can apply URL allowlist or blocklist restrictions in its CDP connection path, but the captured connector does not apply those restrictions to WebDriver BiDi. That is a concrete protocol boundary, and it is another reason to decide how an endpoint is exposed rather than treating connect() as a harmless convenience.

A browser contains contexts, not just tabs

Once connected, Puppeteer does not model the browser as a flat list of pages. A browser has at least one browser context. A context groups pages and owns a defined portion of browser state, including cookies and local storage. Additional contexts can be created for independent sessions, and closing a context closes its pages.

Consider a test that must exercise the same site as two users. Opening two pages in the default context does not, by itself, give those users separate cookies or local storage. Creating two browser contexts does. Each context can then create its own page, navigate, set permissions, and close without deleting the other context’s stored session. Puppeteer’s documentation describes this as isolation of cookies and local storage, not as total isolation of the operating system, filesystem, browser process, or network. That limit matters when deciding whether contexts are sufficient for a security or tenancy boundary.

A context also gives browser-wide state a more precise home. Permission overrides are configured for a context and origin. In CDP-backed operation, context creation can include proxy and download behavior options. Browser-level cookie methods are shortcuts for the default context; the same operations can be performed directly on a chosen context. Thinking in terms of ownership prevents accidental setup in the wrong session.

The default context has a special lifecycle: it exists when the browser starts and cannot be closed like a separately created context. A non-default context can be treated as a disposable unit of work. That makes a context useful for test isolation, temporary authenticated sessions, and parallel tasks, provided the application does not require stronger boundaries than the browser offers.

Pages sit inside this context boundary. A Page represents one tab-like surface, and one browser can expose many pages. A page can navigate, receive input, evaluate JavaScript, observe network events, and produce screenshots or PDFs. It can also contain multiple frames. The frame tree is not a collection of independent pages: JavaScript evaluated in one frame does not automatically affect nested frames, and a frame can be attached, navigated, swapped, or detached while the page remains.

A popup makes the ownership rule visible. If a page opens another page with window.open, Puppeteer associates the popup with the parent page’s browser context. The popup is a new target and page, but it inherits the context relationship. Code that waits for a popup therefore needs to watch the browser or context target lifecycle, not assume that the original page object will turn into the new page.

Workers add another layer. A page can expose workers created by the browser, and Puppeteer emits worker-created and worker-destroyed events. A service worker or extension background worker may be the place where the behavior under test occurs even though it has no visible tab. The browser can be healthy while a particular worker target disappears. The right response depends on the target’s role, not on the fact that a page exists somewhere in the same browser.

Targets are the moving parts the browser reports

A target is a browser entity that can be addressed through the underlying protocol. In Puppeteer’s CDP model, targets include pages and workers and are discovered, attached, changed, and destroyed as the browser runs. The API exposes browser and context events for target creation, URL changes, and destruction, and provides waitForTarget() to match an existing or newly created target.

This is why waiting for a target is different from waiting for a selector. A selector asks whether a DOM object in a known execution context meets a condition. A target wait asks whether a browser entity with a property—perhaps a URL, type, or opener relationship—has appeared. To test an extension popup, for example, the relevant event may be a new page target. To test a service worker, the relevant target may have a worker type and a script URL. To handle a new window, a URL predicate can identify the target after the page opens it.

Target events also explain why object references have lifetimes. A page can navigate and receive a new execution context. A frame can detach. A worker can be destroyed. A protocol connection can close and invalidate all sessions. Puppeteer’s higher-level objects expose these transitions, but they do not make the old entity immortal. Code that stores a handle, frame, or target must either use it within the lifetime it represents or respond to the transition that invalidates it.

The distinction between page and target is especially useful for diagnosis. A target may be discovered before it is fully initialized. Puppeteer’s CDP browser implementation tracks target initialization and exposes a target only when the relevant setup succeeds. If initialization fails, the missing page is not necessarily evidence that navigation or the site’s DOM was wrong; the failure may have occurred at attachment or protocol setup.

The same model applies to navigation. Puppeteer treats a navigation as anything that changes the page URL, including an anchor change or History API use. A single-page application can therefore navigate from the automation library’s point of view without fetching a completely new document. Conversely, a page can remain visually similar while its frame or execution context changes. The event boundary is more reliable than the appearance of the page alone.

The practical pattern is to wait on the level at which the change occurs. If a user action opens a new window, wait for a target whose URL or type identifies that window. If an extension starts a service worker, wait for a worker target rather than searching the visible page for its code. If an iframe navigates, keep the frame relationship and its lifecycle in view instead of assuming that a handle from the old document remains valid. Puppeteer’s target waits consider both targets that already exist and target-created or target-changed events, which lets the same predicate cover a race between “the target appeared just before the wait” and “the target will appear just after it.”

This event-driven model is also a limit on what the library can infer. A target’s existence says that a browser entity has been discovered or initialized; it does not prove that the application has reached the business state a test cares about. A page can be attached but still loading data, and a worker can exist but not yet have performed the relevant work. Target lifecycle tells the script where to observe; a separate readiness condition must tell it what success means.

classDiagram class Browser { close() disconnect() } class BrowserContext class Page class Frame class Worker class Target class ProtocolSession Browser "1" --> "1..*" BrowserContext : contains BrowserContext --> Page : owns Page --> Frame : exposes Page --> Worker : observes Page ..> Target : represented by Target ..> ProtocolSession : addressed through Browser ..> ProtocolSession : connects through

Cleanup follows the hierarchy

A sound cleanup strategy works from the owner outward. If the script created a temporary context, closing that context is the most precise way to close its pages and discard its context-scoped state. If it created the browser process, closing the browser is the appropriate terminal operation. If it only attached to a shared browser, disconnecting may be the correct action so other clients and pages survive.

The same principle applies to page-side resources. Element and JavaScript handles preserve references in an execution context, but Puppeteer documents that they are auto-disposed when the associated frame is navigated away or the parent context is destroyed. Explicit disposal is still relevant for handles kept during a longer operation. Cleanup is not just about avoiding memory growth; it makes the lifetime of a reference match the lifetime of the object it describes.

Reconnection is possible because the browser endpoint can be retained before disconnecting. A client can record the WebSocket endpoint, detach, and later call connect() again. That is useful when a test harness, debugger, or supervisory process must temporarily release control without ending the browser. It also reinforces the central rule: the endpoint identifies a connection route, not necessarily ownership of the browser process.

The hierarchy gives a practical reading of Puppeteer code. Ask first whether the problem is at the browser or connection root. Then ask which context owns the state, which page or target owns the behavior, which frame owns the execution context, and whether a worker or popup is the actual participant. Only after that should the script’s selector, wait condition, or assertion be revised.

It also gives a practical reading of parallel work. Two pages in one context are two views into one state boundary; two contexts are separate storage boundaries inside the same browser. A browser-level event can therefore affect more than the page currently under inspection, while a context-level cleanup can be narrower. The choice is not automatically “one browser per task” or “one page per task.” It depends on whether the task needs shared browser resources, separate session state, or independent process ownership.

That order prepares the next issue. Knowing which object exists does not tell us that it is ready for an action. A page can contain the right element while it is hidden, disabled, moving, or about to be replaced by navigation. The next chapter examines how Puppeteer turns those readiness conditions into part of an interaction rather than leaving them to timing guesses.

References