cdpfleet Docs GitHub Dashboard

Cases / #49 · 2026-10-15 · Medium

The same Camoufox device across relaunches: what a persistent profile keeps, and what it doesn't

A Camoufox profile brought back the same screen, cores, GPU and user agent — and the login — on every relaunch, where launches without one got a new device each time. Two things still change per launch: the canvas noise, and the timezone if the exit IP moves.

Camoufox

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

The problem

Accounts that are used for weeks get flagged when every visit looks like a different computer. Camoufox builds a believable persona on each launch — which is exactly the problem for a long-lived account: a new screen, GPU and CPU count every time. Does a persistent profile really bring the same device back, along with the login, and what does a fingerprinting script still see change?

What we used, and why

WhatWhy
"profile": "…"The first launch creates the profile — Camoufox stores the persona it generated with the cookies and storage; later launches restore it. Work in browser.contexts()[0].
"os": "windows"Only used when the profile is created; later launches keep the stored persona whatever you pass.
PROXY_URL_USOne country for every launch: Camoufox sets timezone and locale from the exit IP.
A small fingerprinting scriptUser agent, platform, CPU cores, screen, WebGL renderer and a canvas hash — read on httpbin.org, plus a cookie and a localStorage entry as the "login".
GET / DELETE /v1/me/profilesWait until each save has landed before the next launch, and delete the profile at the end.

How it works

  1. Launch Camoufox four times with one new profile; on the first launch set a session cookie and a localStorage value; on each launch read the fingerprint (drawing the canvas twice).
  2. Wait for the profile's new version to be saved between launches.
  3. Launch twice more without a profile, for contrast, then delete the profile.

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_US
import { createHash, randomBytes } from 'node:crypto';
import { firefox } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;
const NAME = `case49-node-${randomBytes(4).toString('hex')}`; // one profile per run
const SITE = 'https://httpbin.org/html';
// One country for every launch: Camoufox matches timezone and locale to the exit IP.
const BODY = { proxy: process.env.PROXY_URL_US, headless: true, os: 'windows', record: false };

async function api(method, path) {
  const res = await fetch(`https://cdpfleet.com${path}`, { method, headers: { 'x-api-key': KEY } });
  return { status: res.status, body: await res.json().catch(() => ({})) };
}
// The node saves the profile after the session's browser has exited: wait for the new version.
async function waitForVersion(version) {
  for (let i = 0; i < 60; i++) {
    const p = (await api('GET', '/v1/me/profiles')).body.profiles?.find((x) => x.name === NAME);
    if (p?.version >= version && !p.in_use) return p;
    await new Promise((r) => setTimeout(r, 2000));
  }
  throw new Error(`profile ${NAME} never reached version ${version}`);
}

// 503 means the fleet is momentarily busy: wait Retry-After and try again.
async function launch(body) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch('https://starter.cdpfleet.com/camoufox/session', {
      method: 'POST',
      headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
      body: JSON.stringify(body),
    });
    if (res.ok) return res.json();
    if (res.status !== 503 || attempt === 5) throw new Error(`launch ${res.status} ${await res.text()}`);
    await new Promise((r) => setTimeout(r, (Number(res.headers.get('retry-after')) || 5) * 1000));
  }
}

// Proxies drop a tunnel now and then: up to two retries.
async function go(page, url) {
  for (let attempt = 1; ; attempt++) {
    try {
      return await page.goto(url, { timeout: 60000 });
    } catch (err) {
      if (attempt === 3) throw err;
      await new Promise((r) => setTimeout(r, 2000));
    }
  }
}

// What a fingerprinting script reads, plus the "login" kept in a cookie and localStorage.
async function visit(page, { logIn }) {
  await go(page, SITE);
  if (logIn) await page.evaluate(() => { document.cookie = 'session=alice-7f3; max-age=604800; path=/'; localStorage.setItem('user', 'alice'); });
  const fp = await page.evaluate(() => {
    const gl = document.createElement('canvas').getContext('webgl');
    const dbg = gl && gl.getExtension('WEBGL_debug_renderer_info');
    const draw = () => {
      const c = document.createElement('canvas');
      c.width = 240; c.height = 60;
      const ctx = c.getContext('2d');
      ctx.font = '18px Arial'; ctx.fillStyle = '#f60'; ctx.fillRect(10, 10, 100, 30); ctx.fillStyle = '#069'; ctx.fillText('cdpfleet device 49', 4, 40);
      return c.toDataURL();
    };
    return {
      user_agent: navigator.userAgent,
      platform: navigator.platform,
      cores: navigator.hardwareConcurrency,
      screen: `${screen.width}x${screen.height}`,
      webgl_renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null,
      canvas: draw(),
      canvas_again: draw(), // the same drawing twice in one session
      timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
      languages: navigator.languages,
      login_cookie: document.cookie.includes('session=alice-7f3'),
      login_storage: localStorage.getItem('user') === 'alice',
    };
  });
  const hash = (x) => createHash('sha256').update(x).digest('hex').slice(0, 12);
  fp.canvas_same_in_session = fp.canvas === fp.canvas_again;
  fp.canvas = hash(fp.canvas);
  delete fp.canvas_again;
  return fp;
}

async function session(body, logIn) {
  const s = await launch(body);
  const browser = await firefox.connect(s.wsUrl, { headers: { 'x-api-key': KEY } });
  try {
    // With a profile, its context is browser.contexts()[0]; without one, a fresh context.
    const context = body.profile ? browser.contexts()[0] : await browser.newContext();
    const page = context.pages()[0] ?? await context.newPage();
    return await visit(page, { logIn });
  } finally {
    await browser.close();
  }
}

// The device's hardware-like values (user agent, platform, cores, screen, GPU); the canvas is
// reported on its own.
const device = (r) => createHash('sha256').update(JSON.stringify([r.user_agent, r.platform, r.cores, r.screen, r.webgl_renderer])).digest('hex').slice(0, 10);
try {
  // Four launches with one profile: the first creates it (device + login), the rest restore it.
  const withProfile = [];
  for (let run = 1; run <= 4; run++) {
    withProfile.push({ run, ...(await session({ ...BODY, profile: NAME }, run === 1)) });
    await waitForVersion(run);
  }
  // Two launches without a profile, for contrast.
  const without = [];
  for (let run = 1; run <= 2; run++) without.push({ run, ...(await session(BODY, false)) });
  const pick = (r) => ({ run: r.run, device: device(r), user_agent: r.user_agent, cores: r.cores, screen: r.screen, webgl_renderer: r.webgl_renderer, canvas: r.canvas, canvas_same_in_session: r.canvas_same_in_session, timezone: r.timezone, login_cookie: r.login_cookie, login_storage: r.login_storage });
  console.log(JSON.stringify({ with_profile: withProfile.map(pick), without_profile: without.map(pick) }, null, 2));
} finally {
  await api('DELETE', `/v1/me/profiles/${NAME}`);
}

What we got

LaunchDevice hashScreen · cores · GPUCanvas hash (same twice in session?)TimezoneLogin kept
profile, 19b4c5e3d222176x1224 · 24 · ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar2aa3626347ab (yes)America/New_Yorkset
profile, 29b4c5e3d222176x1224 · 24 · ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similareee3aa52d041 (yes)America/Chicagoyes
profile, 39b4c5e3d222176x1224 · 24 · ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar3406714b0218 (yes)America/Chicagoyes
profile, 49b4c5e3d222176x1224 · 24 · ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similaredfb4c52ec36 (yes)America/New_Yorkyes
no profile, 1e57979ea7d2176x1224 · 8 · ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar3f49470cb734 (yes)America/New_York—
no profile, 2de4852f49f1920x1080 · 8 · ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar42bb3f9ae210 (yes)America/New_York—

From the Node.js run on 2026-10-15. 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)
{
  "with_profile": [
    {
      "run": 1,
      "device": "9b4c5e3d22",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0",
      "cores": 24,
      "screen": "2176x1224",
      "webgl_renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar",
      "canvas": "2aa3626347ab",
      "canvas_same_in_session": true,
      "timezone": "America/New_York",
      "login_cookie": true,
      "login_storage": true
    },
    {
      "run": 2,
      "device": "9b4c5e3d22",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0",
      "cores": 24,
      "screen": "2176x1224",
      "webgl_renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar",
      "canvas": "eee3aa52d041",
      "canvas_same_in_session": true,
      "timezone": "America/Chicago",
      "login_cookie": true,
      "login_storage": true
    },
    {
      "run": 3,
      "device": "9b4c5e3d22",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0",
      "cores": 24,
      "screen": "2176x1224",
      "webgl_renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar",
      "canvas": "3406714b0218",
      "canvas_same_in_session": true,
      "timezone": "America/Chicago",
      "login_cookie": true,
      "login_storage": true
    },
    {
      "run": 4,
      "device": "9b4c5e3d22",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0",
      "cores": 24,
      "screen": "2176x1224",
      "webgl_renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar",
      "canvas": "edfb4c52ec36",
      "canvas_same_in_session": true,
      "timezone": "America/New_York",
      "login_cookie": true,
      "login_storage": true
    }
  ],
  "without_profile": [
    {
      "run": 1,
      "device": "e57979ea7d",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0",
      "cores": 8,
      "screen": "2176x1224",
      "webgl_renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar",
      "canvas": "3f49470cb734",
      "canvas_same_in_session": true,
      "timezone": "America/New_York",
      "login_cookie": false,
      "login_storage": false
    },
    {
      "run": 2,
      "device": "de4852f49f",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0",
      "cores": 8,
      "screen": "1920x1080",
      "webgl_renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar",
      "canvas": "42bb3f9ae210",
      "canvas_same_in_session": true,
      "timezone": "America/New_York",
      "login_cookie": false,
      "login_storage": false
    }
  ]
}

Takeaways