Skip to main content

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.

note

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/bql to something like https://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/chromium to something like wss://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.

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);
});

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);
}