DEV Community

Cover image for BitBrowser + Puppeteer: Automating Multi-Profile Browser Sessions
Digital Growth Pro
Digital Growth Pro

Posted on

BitBrowser + Puppeteer: Automating Multi-Profile Browser Sessions

Puppeteer is one of the most widely used tools for browser automation — but it has a well-known limitation when it comes to multi-profile operations: every Chromium instance it launches shares the same underlying browser fingerprint. For tasks that require isolated, independent browser identities across multiple sessions, standard Puppeteer falls short.

BitBrowser solves this by providing persistent, isolated browser profiles with unique fingerprints per session. Each profile has its own Canvas signature, WebGL renderer, screen parameters, fonts, timezone, and User Agent — and exposes a remote debugging port that Puppeteer can connect to directly. The result is a Puppeteer automation stack where each session carries a genuinely distinct browser identity.

This guide covers the full integration: connecting Puppeteer to BitBrowser profiles via the remote debugging API, managing multiple profile sessions, and structuring a multi-profile automation workflow.


How BitBrowser Exposes Browser Sessions to Puppeteer

BitBrowser is built on Chromium and supports Chrome DevTools Protocol (CDP) — the same protocol Puppeteer uses to control browser instances. When you launch a profile through BitBrowser's local API, it starts a Chromium instance with that profile's fingerprint parameters loaded, and exposes a WebSocket debugging endpoint that Puppeteer can attach to using puppeteer.connect().

This is different from puppeteer.launch(), which starts a fresh Chromium instance with no persistent fingerprint. With puppeteer.connect(), Puppeteer attaches to an already-running browser session — in this case one that BitBrowser has fully initialized with the correct fingerprint, proxy, and session data for that profile.


Prerequisites

  • Node.js 18 or higher
  • Puppeteer: npm install puppeteer-core
  • BitBrowser installed and running locally
  • At least one configured profile in BitBrowser with a proxy assigned

Note: use puppeteer-core rather than the full puppeteer package since BitBrowser provides its own Chromium binary. You do not need Puppeteer to download a separate browser.


Step 1: Start a Profile via BitBrowser's Local API

BitBrowser exposes a local REST API on http://127.0.0.1:54345 that allows you to open, close, and query the status of browser profiles programmatically.

To open a profile and retrieve its debugging endpoint:

const axios = require('axios');

const BITBROWSER_API = 'http://127.0.0.1:54345';

async function openProfile(profileId) {
  const response = await axios.post(`${BITBROWSER_API}/browser/open`, {
    id: profileId
  });

  const { data } = response;

  if (data.success) {
    // Returns the WebSocket debugger URL for this profile
    return data.data.ws;
  } else {
    throw new Error(`Failed to open profile: ${data.msg}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

The ws field in the response contains the WebSocket URL that Puppeteer will connect to — something like ws://127.0.0.1:9222/devtools/browser/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.


Step 2: Connect Puppeteer to the Running Profile

Once the profile is open and you have the WebSocket endpoint, connect Puppeteer to it:

const puppeteer = require('puppeteer-core');

async function connectToProfile(wsEndpoint) {
  const browser = await puppeteer.connect({
    browserWSEndpoint: wsEndpoint,
    defaultViewport: null
  });

  return browser;
}
Enter fullscreen mode Exit fullscreen mode

Setting defaultViewport: null tells Puppeteer not to override the viewport — important here because the viewport is part of the fingerprint that BitBrowser has already configured for the profile. Overriding it would create a mismatch between the reported screen parameters and the actual viewport.


Step 3: Full Single-Profile Automation Flow

Putting the two steps together into a complete single-profile session:

const puppeteer = require('puppeteer-core');
const axios = require('axios');

const BITBROWSER_API = 'http://127.0.0.1:54345';

async function runProfileSession(profileId, taskFn) {
  let browser = null;

  try {
    // Open the profile and get the debugger endpoint
    const openRes = await axios.post(`${BITBROWSER_API}/browser/open`, {
      id: profileId
    });

    if (!openRes.data.success) {
      throw new Error(`Could not open profile ${profileId}: ${openRes.data.msg}`);
    }

    const wsEndpoint = openRes.data.data.ws;

    // Connect Puppeteer to the running profile
    browser = await puppeteer.connect({
      browserWSEndpoint: wsEndpoint,
      defaultViewport: null
    });

    // Get the existing page or open a new one
    const pages = await browser.pages();
    const page = pages.length > 0 ? pages[0] : await browser.newPage();

    // Run the task passed in as a function
    await taskFn(page);

  } catch (error) {
    console.error(`Session error for profile ${profileId}:`, error.message);
  } finally {
    // Disconnect Puppeteer without closing the browser
    // This preserves the session state in BitBrowser
    if (browser) {
      await browser.disconnect();
    }

    // Close the profile via BitBrowser API
    await axios.post(`${BITBROWSER_API}/browser/close`, { id: profileId });
  }
}
Enter fullscreen mode Exit fullscreen mode

The browser.disconnect() call is important: it detaches Puppeteer from the session without closing the Chromium instance. This allows BitBrowser to cleanly save the session state — cookies, localStorage, login state — before the profile is closed via the API.


Step 4: Multi-Profile Sequential Automation

For multi-profile workflows, the safest approach is running profiles sequentially rather than in parallel. This avoids resource contention on lower-spec machines and prevents any timing-based behavioral clustering across sessions:

const profiles = [
  { id: 'profile_id_1', name: 'Account A' },
  { id: 'profile_id_2', name: 'Account B' },
  { id: 'profile_id_3', name: 'Account C' }
];

async function runAllProfiles() {
  for (const profile of profiles) {
    console.log(`Running session for: ${profile.name}`);

    await runProfileSession(profile.id, async (page) => {
      await page.goto('https://example.com/dashboard');
      await page.waitForSelector('#main-content');

      // Your per-profile task logic here
      const title = await page.title();
      console.log(`${profile.name} - Page title: ${title}`);
    });

    // Add a delay between profile sessions
    // Avoids identical session timing patterns across accounts
    await new Promise(resolve => setTimeout(resolve, 3000 + Math.random() * 2000));
  }
}

runAllProfiles().catch(console.error);
Enter fullscreen mode Exit fullscreen mode

The randomized delay between sessions (3000 + Math.random() * 2000 milliseconds) is deliberate. Identical session start times across multiple accounts is a behavioral pattern that analytics systems can detect. Adding variance makes the timing look more organic.


Step 5: Parallel Sessions for Higher Throughput

If sequential execution is too slow for your use case, you can run profiles in parallel — but limit concurrency to avoid overwhelming system resources:

async function runInBatches(profiles, batchSize, taskFn) {
  for (let i = 0; i < profiles.length; i += batchSize) {
    const batch = profiles.slice(i, i + batchSize);

    await Promise.all(
      batch.map(profile =>
        runProfileSession(profile.id, taskFn)
      )
    );

    console.log(`Completed batch ${Math.floor(i / batchSize) + 1}`);

    // Pause between batches
    await new Promise(resolve => setTimeout(resolve, 5000));
  }
}

// Run 3 profiles at a time
await runInBatches(profiles, 3, async (page) => {
  await page.goto('https://example.com');
  // task logic
});
Enter fullscreen mode Exit fullscreen mode

Batch size depends on your machine's available memory. Each BitBrowser profile runs a full Chromium instance, which typically consumes 200–400MB RAM. For a machine with 16GB RAM, a batch size of 4–6 is a reasonable starting point.


Retrieving Profile IDs Programmatically

Rather than hardcoding profile IDs, you can query the BitBrowser API to list all available profiles:

async function listProfiles() {
  const response = await axios.get(`${BITBROWSER_API}/browser/list`, {
    params: {
      page: 0,
      pageSize: 100
    }
  });

  if (response.data.success) {
    return response.data.data.list.map(profile => ({
      id: profile.id,
      name: profile.name
    }));
  }

  return [];
}

const profiles = await listProfiles();
console.log(`Found ${profiles.length} profiles`);
Enter fullscreen mode Exit fullscreen mode

This makes your automation scripts portable — they discover available profiles at runtime rather than relying on hardcoded IDs that break when profiles are renamed or reorganized.


Handling Profile Open Failures Gracefully

In production automation, profiles occasionally fail to open — the BitBrowser API might time out, a proxy might be unreachable, or the Chromium instance might take longer than expected to initialize. A simple retry wrapper prevents single-profile failures from breaking your entire batch:

async function openProfileWithRetry(profileId, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await axios.post(`${BITBROWSER_API}/browser/open`, {
        id: profileId
      });

      if (response.data.success) {
        return response.data.data.ws;
      }

      throw new Error(response.data.msg);

    } catch (error) {
      console.warn(`Profile ${profileId} open attempt ${attempt} failed: ${error.message}`);

      if (attempt < maxRetries) {
        await new Promise(resolve => setTimeout(resolve, 2000 * attempt));
      } else {
        throw new Error(`Profile ${profileId} failed after ${maxRetries} attempts`);
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The exponential backoff (2000 * attempt) gives the system time to recover between retries without flooding the API with rapid repeated requests.


Key Integration Notes

Always use browser.disconnect(), not browser.close() — closing the browser through Puppeteer bypasses BitBrowser's session-saving logic. Disconnecting and then closing through the API preserves cookie and localStorage state correctly.

Do not override viewportdefaultViewport: null is mandatory. The viewport dimensions are part of the fingerprint configuration BitBrowser sets per profile. Overriding them creates a detectable inconsistency.

Proxy is handled at the profile level — you do not need to configure proxy settings in Puppeteer. BitBrowser loads the proxy assigned to each profile automatically when it opens. Your Puppeteer code connects to an already-proxied session.

Session persistence is per profile — cookies, localStorage, and login state are saved per profile by BitBrowser between sessions. Your automation scripts can rely on previously authenticated sessions without needing to re-login on every run.


Practical Applications

This integration is used for a range of legitimate automation tasks: automated account health checks across multiple managed accounts, scheduled content operations across isolated profiles, data collection workflows that require distinct browser identities per session, and QA testing scenarios that need to verify behavior across multiple independent user contexts.

For workflows that extend to mobile environments — where some platforms behave differently or provide different data through mobile sessions — BitCloudPhone provides isolated Android environments that can be integrated into the same operational stack alongside desktop BitBrowser profiles.

If you have not set up BitBrowser yet, register here to get access to the local API and start building profile-based automation workflows. The API documentation covers additional endpoints for profile creation, configuration updates, and status monitoring beyond what this guide covers.

Top comments (0)