How to Create a Browser Profile with Puppeteer
Use Puppeteer’s userDataDir option to give Chrome a persistent on-disk profile, or create isolated browser contexts when you only need separate storage.
To create a persistent browser profile with Puppeteer, pass a writable directory to userDataDir when you launch the browser. Chrome stores profile data such as cookies and local storage there, so later launches using the same directory can reuse that on-disk data. If you only need separate storage for tasks during one browser run, create separate BrowserContext instances instead.
The distinction matters: userDataDir selects the browser process’s on-disk user data directory; a browser context isolates storage inside a running browser. Puppeteer documents userDataDir as a path to a user data directory. Puppeteer LaunchOptions
1. Create a persistent profile with userDataDir
Install Puppeteer, save the following as profile.mjs, and run it with Node.js. Replace /tmp/puppeteer-profile with a directory the process can write to. The first launch initializes the directory; subsequent launches that use the same path can reuse its profile data.
npm install puppeteer
# Save the following as profile.mjs, then run:
node profile.mjs
import puppeteer from 'puppeteer';
const profilePath = '/tmp/puppeteer-profile';
const browser = await puppeteer.launch({
userDataDir: profilePath,
});
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log('Title:', await page.title());
console.log('Profile directory:', profilePath);
} finally {
await browser.close();
}
Use an absolute path when practical so the profile location does not depend on the script’s current working directory. The directory must be writable by the account running Chrome. The sample uses /tmp because Puppeteer’s troubleshooting guide gives /tmp/.puppeteer-profile as an example for a constrained container where /tmp is writable. Choose a suitable writable path for your own runtime. Puppeteer troubleshooting
What persists, and what does not
The profile is on disk, but your application should still decide which state it needs to preserve and how to manage the directory. Do not treat a browser context as a named, persistent desktop profile: contexts are created within a browser instance and can be closed independently.
Close a browser launched by your script with browser.close(). If Puppeteer attached to a browser running elsewhere and you only want to detach the client, use browser.disconnect(); disconnecting leaves the browser and its pages running. Puppeteer Browser API
2. Use separate browser contexts for storage isolation
When several tasks need separate cookies and local storage in the same browser process, create one context per task. This avoids assigning a separate on-disk profile path to each task. Puppeteer’s API says each context has isolated storage; in Chrome, non-default contexts are incognito.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const context = await browser.createBrowserContext();
try {
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log(await page.title());
} finally {
await context.close();
}
} finally {
await browser.close();
}
Choose the mechanism that fits the lifetime and scope of the data:
| Need | Use | What it means |
|---|---|---|
| Reuse an on-disk browser user data directory across launches | userDataDir |
The browser process writes profile data to the selected path. That path must be writable. |
| Keep task cookies and local storage separate within one browser run | Separate BrowserContext instances |
Each context isolates its storage; close each context when its task ends. |
These approaches solve related but different problems. A context provides in-browser isolation; userDataDir configures an on-disk directory for the launched browser.
3. Configure the profile for your runtime
Containers and restricted filesystems
Chrome writes profile, configuration, and cache files at startup. In a read-only or constrained environment, direct these files to writable locations. A writable temporary directory may work for an ephemeral run; use a writable persistent volume if the profile needs to survive the container’s lifetime. Confirm that the runtime user can create and modify files in the chosen location. Puppeteer troubleshooting guidance
Keep browser versions compatible
Puppeteer versions map to supported browser versions, and that mapping changes over time. Check the supported-browser table for the Puppeteer release actually installed in your project rather than copying a version number from an older setup guide. Puppeteer supported browsers
Profile path and lifecycle checklist
- Set
userDataDirinpuppeteer.launch(), not on a page or context. - Use a path the Chrome process can write to, including in container deployments.
- Use separate browser contexts when the requirement is per-task cookie and local-storage isolation.
- Close contexts after their tasks, then close a browser process your code launched.
- Check the supported-browser table when changing Puppeteer versions.
4. Troubleshoot common profile problems
| Symptom | Likely cause | What to do |
|---|---|---|
| Chrome fails during startup when a profile path is configured | The process cannot write to the profile directory, or another required profile, configuration, or cache location is not writable. | Choose a writable directory and check permissions for the account running Chrome. In a constrained container, direct Chrome’s writable files to locations the container permits. |
| The profile appears empty or state is missing on the next run | The next run used a different directory, or the selected storage was a context that was closed rather than a reused on-disk profile. | Pass the same userDataDir path on each launch when you intend to reuse the disk profile. Use contexts when you intend temporary isolation within a running browser. |
| Two tasks see different cookies than expected | They may be using separate contexts, which isolate cookies and local storage. | Use the same intended context for tasks that need shared context storage, or intentionally keep separate contexts for isolation. |
| The browser keeps running after the script’s Puppeteer client detaches | browser.disconnect() detaches without shutting down the browser. |
For a browser launched by the script and meant to end with it, call browser.close(). Use disconnect only when leaving the browser running is intended. |
| Browser launch behavior changes after a Puppeteer upgrade | The installed Puppeteer release may support a different browser version. | Consult Puppeteer’s current supported-browser table for the installed release and follow its installation guidance. |
A profile path error is a filesystem problem to investigate first. Do not add browser flags as a substitute for making the required directories writable; Puppeteer’s troubleshooting guidance specifically calls out writable profile, configuration, and cache paths.
5. Performance, reliability, and cost
A persistent profile keeps browser state on disk, which is useful when later launches need that state. It also means the runtime must be able to read and write the selected directory. In an ephemeral environment, a temporary path can disappear when the environment ends, so it will not serve as durable storage across recreated containers.
Browser contexts provide a practical way to separate task storage within one browser process. Close contexts after use, and close the launched browser when the process should stop. This makes the intended lifetime of each browser resource explicit.
Puppeteer is a self-managed browser automation approach: your environment runs Chrome and must provide its writable filesystem locations. For a screenshot-only workflow, browser installation and profile management may be unnecessary. ScreenshotNeo provides a screenshot API and MCP server for developers.
Or skip the browser setup
For a screenshot without managing a Puppeteer browser profile, make one request to ScreenshotNeo. See the ScreenshotNeo API documentation for options and response details.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and responses include X-Page-Verdict and X-Billed headers. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for free ScreenshotNeo screenshots.
FAQ
Does Puppeteer create a profile directory automatically?
userDataDir selects the user data directory for the browser launch. Ensure the path is writable and use the same path on later launches when you want to reuse that on-disk profile.
Can I use a BrowserContext as a persistent named profile?
Contexts isolate storage within a browser instance. Use userDataDir when you need an on-disk user data directory configured at launch.
Should I close the browser or disconnect?
Close a browser launched by your script when it should stop. Disconnect when Puppeteer is attached to a browser that should remain running.
Where can I check which browser version matches my Puppeteer version?
Use Puppeteer’s supported-browser table and check the entry for the release installed in your project.


