skip to content

What does Playwright's chromium.launchPersistentContext(userDataDir) buy you, and what does it cost?

level: middleimportance: should knowfreq 37%

answer

  1. A directory on disk, not memory
  2. Returns a context, never a browser
  3. One context, closing it ends everything
  4. Extensions need a real profile
  5. Two runs cannot share one directory

basics

~10 s

It launches a browser against a profile directory on disk and returns the single BrowserContext that owns it. You gain state that survives runs plus Chromium extensions; you lose fresh isolation and independent contexts.

solid answer

~40 s

`browserType.launchPersistentContext(userDataDir, options)` starts a browser whose profile lives in the directory you name and returns a **`BrowserContext`**, not a `Browser`. That context is the only one - closing it closes the browser. Because there is a real Chromium profile on disk, cookies and local storage survive between runs, and Chromium extensions can be side-loaded with `--load-extension` in `args`, which plain `launch()` cannot do. In Playwright 1.63 you add `channel: 'chromium'` to run those extensions headlessly. The cost is isolation: every test sharing that directory shares its state, so leftovers from an earlier hotel-booking session travel forward. Chromium also refuses to launch two instances against the same `userDataDir`, so parallel runs need separate directories. Passing an empty string gives you a temporary profile instead.

code

typescript · 14 lines
typescript
import { chromium } from '@playwright/test';
import path from 'path';

const extension = path.join(__dirname, 'booking-helper-extension');
const context = await chromium.launchPersistentContext('/tmp/pw-booking-profile', {
  channel: 'chromium',
  args: [
    `--disable-extensions-except=${extension}`,
    `--load-extension=${extension}`,
  ],
});
const page = await context.newPage();
await page.goto('https://staging.example.com/hotels/search');
await context.close();

go deeper

for a junior

Remember that this call takes a directory path, returns a context rather than a browser, and that closing that context shuts the browser down as well.

for a middle

Explain that the profile lives on disk, so state carries between runs, and that Chromium extensions need exactly that profile, which plain launch cannot provide.

for a senior

Show that you weigh the loss of fresh isolation, manage the directory as a fixture with a lifecycle, and give concurrent runs separate paths.

for a principal

Own the policy on where persistent profiles are allowed at all, since each one is an isolation exception that the whole suite pays for in debugging time.

`browserType.launchPersistentContext(userDataDir, options)` is the second way to start a browser in Playwright. Where `launch()` gives you a `Browser` you then carve contexts out of, this call starts the browser **against a profile directory on disk** and hands back the single `BrowserContext` that owns it. ## What the call actually returns - The return value is a **`BrowserContext`**, not a `Browser`. There is no `newContext()` step, because the persistent profile is the context. - Closing that context closes the browser process with it - the two lifetimes are welded together. - It accepts both browser-level options (`headless`, `channel`, `args`, `proxy`, `slowMo`) and context-level options in the same call, since it creates both in one go. - Passing an empty string as `userDataDir` gives you a temporary directory instead of a durable one, which is useful when you want the persistent-context *shape* without the persistence. ## What the profile buys you 1. **State that survives the process.** Cookies, local storage and other profile data written during one run are still there for the next one, because they live in a directory rather than in memory. 2. **Chromium extensions.** Extensions load only into a real profile, so side-loading one with `args: ['--disable-extensions-except=<path>', '--load-extension=<path>']` requires a persistent context. Plain `launch()` has nowhere to put it. 3. **Headless extension runs.** In Playwright 1.63 you add `channel: 'chromium'` to get Chromium's new headless mode, which supports extensions; otherwise the run has to be headed. ## What it costs | | `launch()` | `launchPersistentContext(userDataDir)` | |---|---|---| | Returns | `Browser` | `BrowserContext` | | Contexts | as many as you create | exactly one | | Starting state | clean every time | whatever the directory holds | | Extensions in Chromium | not supported | supported via `args` | | Two runs at once | fine | not against the same directory | The isolation column is the real trade. A fresh context is Playwright's cheapest guarantee that one test cannot poison another; a persistent profile removes it deliberately. Concretely: - A guest profile left signed in by yesterday's run makes today's "signed-out visitor sees the search page" test pass for the wrong reason - or fail for one. - Cached responses and service-worker registrations from an earlier build linger in the directory. - Browsers refuse to launch two instances against the same `userDataDir`, so two workers or two developers cannot share one path; each needs its own. - Chrome policy changes mean you should never point `userDataDir` at your everyday Chrome profile. Create a dedicated, empty automation directory instead. ## When it is the right call - You are testing a browser extension against the hotel-booking site - there is no alternative. - You need a profile-level behaviour that only a real profile exhibits, such as a permission decision the browser stores on disk. - You are reproducing something by hand and want the browser to come back the way you left it. ## When it is the wrong call - **As a way to skip signing in.** Reusing a saved session is its own mechanism and does not require giving up isolated contexts; a persistent profile is a heavy way to get the same thing and drags along the rest of the profile. - **As a default for a suite.** You trade the strongest isolation guarantee in the tool for state you did not ask for, and you inherit a per-directory launch constraint. ## Practical rules 1. Treat the directory as a **fixture with a lifecycle** - know who creates it, who cleans it, and whether it is committed, cached or thrown away. 2. Give every concurrent run its own path, generated per worker or per run. 3. Keep it out of the default browser setup; reach for it only for the cases above, and say in a comment which one applies. 4. If a test depending on the profile starts failing mysteriously, delete the directory first - a corrupted or stale profile is a common cause and costs nothing to rule out.

  • Why can two Playwright workers not share one userDataDir?
    Browsers lock a profile directory to one running instance, so a second launch against the same path fails or corrupts the profile. Give each worker its own generated directory, or use an empty string so Playwright allocates a temporary one per launch.
  • How do you keep the state in a persistent profile from leaking between tests?
    You largely cannot while sharing one directory - that is the cost of the mode. Either recreate the directory before each test that must start clean, or accept that the profile is shared and reserve persistent contexts for the few cases, such as extensions, that genuinely need them.

saying these in an interview costs you the question

  • Thinks launchPersistentContext returns a Browser object
  • Calls newContext on it for isolation between tests
  • Points userDataDir at their everyday Chrome profile
  • Shares one profile directory across parallel runs
  • Uses a persistent profile just to stay signed in