User Data Directory
The --user-data-dir flag persists browser data (cookies, localStorage, cache, and login sessions) across multiple browser sessions. Use it to maintain authenticated states and user preferences without re-authenticating each time.
The --user-data-dir feature is available for self-hosted Docker deployments and Private Deployment (dedicated fleet) plans. It is not available on shared cloud plans.
Deployment-Specific Setup
Self-Hosted Docker
When running the enterprise Docker image, --user-data-dir works out of the box. Pass it as a Chrome flag in the launch parameter or via the args array. The directory path is local to the container, so mount a Docker volume if you need data to persist across container restarts.
Example connection:
ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE&launch={"args":["--user-data-dir=~/u/1"]}
The launch parameter must be Base64-encoded before sending. See Launch Options for encoding details.
Private Deployment (Dedicated Fleet)
On dedicated fleets, user data directories are stored locally on each worker's file system. You must connect to the specific worker where the data was created. See the section below on pointing to a specific dedicated worker for details.
Implementation Details
Browserless creates the directory if it does not exist. Use a unique directory path for each browser profile. Only one browser can use a given --user-data-dir path at a time. You cannot share the same path across multiple concurrent browsers.
Pointing to a specific dedicated worker on Enterprise plans
This section applies to any deployment with multiple workers in the fleet, whether Private Deployment or self-hosted.
User data directories are stored locally on each worker's file system and do not sync across your fleet. For example, a browser instance using --user-data-dir=~/u/1 on Worker #1 will not be accessible from Worker #2. To handle this, point your connection at a specific worker. In your account page, open your production cluster and go to the workers section. Click the IP address of a worker to copy its direct endpoint. Keep track of which worker holds each browser profile to ensure consistent access.
- Your BQL endpoints would change from
https://chrome.browserless.io/bqlto something likehttps://chrome.browserless.io/p/53616c7465645f5ff8cc738d5eecb3032823d67e37578fe4531b0f9a83dc80856c66d0fe36aba4d2f4bc5f01c18bdfab/bql?token=YOUR_API_TOKEN_HERE&--user-data-dir=~/custompath/123 - Your websocket connections would change from
wss://chrome.browserless.io/chromiumto something likewss://chrome.browserless.io/p/53616c7465645f5ff8cc738d5eecb3032823d67e37578fe4531b0f9a83dc80856c66d0fe36aba4d2f4bc5f01c18bdfab/chromium?token=YOUR_API_TOKEN_HERE&--user-data-dir=~/custompath/456
Using User Data Directory with Puppeteer and Playwright
These examples persist a login session to browserless.io. After logging in and closing the tab, running the script again shows you already logged in.
- Puppeteer
- Playwright
import puppeteer from 'puppeteer-core';
const launchArgs = {
headless: false,
args: ['--user-data-dir=~/u/1']
};
const queryParams = new URLSearchParams({
token: "YOUR_API_TOKEN_HERE", //this script using userdatadir only works for dedicated machines
timeout: 300000,
launch: JSON.stringify(launchArgs)
}).toString();
(async () => {
let browser;
try {
browser = await puppeteer.connect({
browserWSEndpoint: `wss://chrome.browserless.io/chromium?${queryParams}`,
defaultViewport: null
});
console.log('Connected');
const page = await browser.newPage();
await page.goto('https://browserless.io/account/', {
waitUntil: 'domcontentloaded'
});
console.log('Navigated');
const client = await page.target().createCDPSession();
const { liveURL } = await client.send('Browserless.liveURL', {
timeout: 300000
});
console.log('Click for live experience:', liveURL);
await new Promise((resolve) => {
client.on('Browserless.liveComplete', resolve);
});
console.log(`Live URL closed on page: ${page.url()}`);
await browser.close();
} catch (error) {
console.error('An error occurred:', error);
if (browser) {
await browser.close().catch(console.error);
}
throw error;
}
})().catch(error => {
console.error('Fatal error:', error);
});
import { chromium } from 'playwright-core';
const launchArgs = {
headless: false,
stealth: true,
args: ['--user-data-dir=~/u/1']
};
const queryParams = new URLSearchParams({
token: "YOUR_API_TOKEN_HERE", //this script using userdatadir only works for dedicated machines
timeout: 300000,
launch: JSON.stringify(launchArgs)
}).toString();
(async () => {
let browser;
try {
browser = await chromium.connectOverCDP(
`wss://chrome.browserless.io/chromium/stealth?${queryParams}`
);
console.log('Connected');
const [context] = await browser.contexts();
const page = await context.newPage();
await page.goto('https://browserless.io/account/', {
waitUntil: 'domcontentloaded'
});
console.log('Navigated');
const cdpSession = await context.newCDPSession(page);
const { liveURL } = await cdpSession.send('Browserless.liveURL', {
timeout: 300000
});
console.log('Click for live experience:', liveURL);
await new Promise((resolve) => {
cdpSession.on('Browserless.liveComplete', resolve);
});
console.log(`Live URL closed on page: ${page.url()}`);
await browser.close();
} catch (error) {
console.error('An error occurred:', error);
if (browser) {
await browser.close().catch(console.error);
}
throw error;
}
})().catch(error => {
console.error('Fatal error:', error);
});
Using with BrowserQL
The following example demonstrates how to use the --user-data-dir flag with BrowserQL to persist browser state. This example shows how to toggle dark mode on w3schools.com - each time you run it, the initial state will be different since it's persisting the toggle state from previous runs.
import puppeteer from 'puppeteer-core';
const url = 'https://www.browserless.io/';
const token = 'YOUR_API_TOKEN_HERE'; //this script using userdatadir only works for dedicated machines
const timeout = 5 * 60 * 1000;
const launchArgs = {
args: ['--user-data-dir=~/id-togle-test-123']
};
const queryParams = new URLSearchParams({
timeout,
token,
launch: JSON.stringify(launchArgs)
}).toString();
const query = `
mutation DarkModeToggle {
goto(url: "https://www.w3schools.com/", waitUntil: domContentLoaded) {
status
}
DarkModeClassBefore:evaluate(content: "document.body.className") {
value
}
click(selector:"#tnb-dark-mode-toggle-btn"){
time
}
DarkModeClassAfter:evaluate(content: "document.body.className") {
value
}
}
`;
const variables = { url };
const endpoint =
`https://chrome.browserless.io/chromium/bql?${queryParams}`;
const options = {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
query,
}),
};
try {
console.log(`Running BQL Query: ${url}`);
const response = await fetch(endpoint, options);
if (!response.ok) {
throw new Error(`Got non-ok response:\n` + (await response.text()));
}
const { data } = await response.json();
console.log("Full response data:", JSON.stringify(data, null, 2));
if (data && data.DarkModeClassBefore && data.DarkModeClassAfter) {
console.log("Dark mode toggle results:");
console.log("Before:", data.DarkModeClassBefore.value);
console.log("After:", data.DarkModeClassAfter.value);
console.log("Click time:", data.click.time, "ms");
} else {
console.log("Unexpected response structure:", data);
}
} catch (error) {
console.error(error);
}