cdpfleet Docs GitHub Dashboard

Cases / #20 · 2026-10-05 · Easy

Three visitors, one thread: browser contexts instead of sessions

Cookies, storage, locale and clock isolated per persona inside a single browser session — what a context gives you for free, what it shares, and the proxy surprise in the exit IPs.

Chromium

Run on production on 2026-10-05: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

The problem

Running several identities — accounts, A/B variants, regional visitors — is usually done with one browser per identity, which on a fleet means one session and at least one thread each. Playwright's browser contexts promise the same isolation inside one browser: separate cookie jars, storage, permissions, locale and timezone, like incognito windows that don't know about each other. Is the isolation real when the browser is remote, what do the contexts still share, and what does it cost?

What we used, and why

WhatWhy
One chromium session, headless: "new"All three personas live in it: 1 thread in total.
browser.newContext({ locale, timezoneId })Each context gets its own language and clock — New York, São Paulo, Tokyo — without touching the others.
context.addCookies and localStorageA login token per persona, planted before the first page; storage written from the page.
Reading everything after all three existA leak between contexts would only show once the others have written their state.
http://ip-api.com/json from each contextWhere each persona's traffic exits — all three use the session's proxy.

How it works

  1. Launch one headless Chromium session; note the weight from the launch response.
  2. For each persona, create a context with its locale and timezone, plant a session cookie for example.com, open the page and write the persona's name to localStorage.
  3. With all three open, read back from each: document.cookie, the storage value, navigator.language, the resolved timezone, a fixed UTC instant as local time.
  4. Fetch the exit IP from each context and print one row per persona.

The code

The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:

// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;

// Three visitors who must not see each other's state — in ONE browser session (1 thread).
const PERSONAS = [
  { name: 'alice', locale: 'en-US', timezone: 'America/New_York' },
  { name: 'bruno', locale: 'pt-BR', timezone: 'America/Sao_Paulo' },
  { name: 'chie', locale: 'ja-JP', timezone: 'Asia/Tokyo' },
];

const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
  method: 'POST',
  headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
  body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: 'new' }),
});
if (!res.ok) throw new Error(`launch ${res.status} ${await res.text()}`);
const { wsUrl, weight } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
try {
  const contexts = [];
  for (const p of PERSONAS) {
    // Each context is a separate profile: its own cookies, storage, locale and clock.
    const context = await browser.newContext({ locale: p.locale, timezoneId: p.timezone });
    await context.addCookies([{ name: 'session', value: `${p.name}-token`, domain: 'example.com', path: '/' }]);
    const page = await context.newPage();
    await page.goto('https://example.com/', { timeout: 60000 });
    await page.evaluate((n) => localStorage.setItem('owner', n), p.name);
    contexts.push({ p, page });
  }
  const out = [];
  for (const { p, page } of contexts) {
    // Read everything after all three exist, so any leak between them would show.
    const seen = await page.evaluate(() => ({
      cookie: document.cookie,
      storage_owner: localStorage.getItem('owner'),
      language: navigator.language,
      timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
      clock: new Date('2026-10-05T12:00:00Z').toLocaleTimeString(),
    }));
    const ip = await (await page.goto('http://ip-api.com/json/?fields=query', { timeout: 60000 })).json();
    out.push({ persona: p.name, threads: weight, ...seen, exit_ip: ip.query });
  }
  console.log(JSON.stringify(out, null, 2));
} finally {
  await browser.close();
}

What we got

PersonaThreadsdocument.cookielocalStorage ownerLanguageTimezone12:00 UTC shown asExit IP
alice1session=alice-tokenaliceen-USAmerica/New_York8:00:00 AM203.0.113.1
bruno1session=bruno-tokenbrunopt-BRAmerica/Sao_Paulo09:00:00203.0.113.2
chie1session=chie-tokenchieja-JPAsia/Tokyo21:00:00203.0.113.3

From the Node.js run on 2026-10-05. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)
[
  {
    "persona": "alice",
    "threads": 1,
    "cookie": "session=alice-token",
    "storage_owner": "alice",
    "language": "en-US",
    "timezone": "America/New_York",
    "clock": "8:00:00 AM",
    "exit_ip": "203.0.113.1"
  },
  {
    "persona": "bruno",
    "threads": 1,
    "cookie": "session=bruno-token",
    "storage_owner": "bruno",
    "language": "pt-BR",
    "timezone": "America/Sao_Paulo",
    "clock": "09:00:00",
    "exit_ip": "203.0.113.2"
  },
  {
    "persona": "chie",
    "threads": 1,
    "cookie": "session=chie-token",
    "storage_owner": "chie",
    "language": "ja-JP",
    "timezone": "Asia/Tokyo",
    "clock": "21:00:00",
    "exit_ip": "203.0.113.3"
  }
]

Takeaways