cdpfleet Docs GitHub Dashboard

Cases / #11 · 2026-10-01 · Easy

Log in once, stay logged in: carrying a session between browsers

Save cookies and localStorage at the end of one session, restore them in a fresh browser — even a different engine on a different server.

Chromium Firefox

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

The problem

Every cdpfleet session is a brand-new browser: when it closes, its cookies and storage are gone, and you never get the same browser back. Logging in for every session is slow, triggers security checks and burns accounts. You want to log in once and reuse that login in later sessions — maybe even in a different browser.

What we used, and why

WhatWhy
context.storageState()Exports every cookie and each origin's localStorage as JSON — Playwright's portable "logged-in state".
A file on your machineThe state outlives the browser. It holds a live login, so treat it like a password.
newContext({ storageState })Starts a context with that state already loaded, before the first request.
chromium, then firefoxTwo different engines on purpose: the state format is engine-independent.
httpbin.org cookies + localStorageStand-ins for a real site's session cookie and saved draft.

How it works

  1. Session 1 (Chromium): set two cookies and a localStorage value, save the state to a file, close the browser.
  2. Session 2 (Firefox, a new browser on whichever server the fleet picks): open a context from the file and check what the site receives.
  3. For comparison, a context without the state in the same browser.

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, firefox } from 'playwright';
import { writeFileSync, readFileSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';

const KEY = process.env.CDPFLEET_API_KEY;
const STATE_FILE = path.join(tmpdir(), 'cdpfleet-state.json'); // keep it somewhere safe: it holds the login

async function launch(name) {
  const res = await fetch(`https://starter.cdpfleet.com/${name}/session`, {
    method: 'POST',
    headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
    body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: true }),
  });
  if (!res.ok) throw new Error(`launch ${name}: ${res.status} ${await res.text()}`);
  return res.json();
}

const cookiesSeen = async (page) => (await (await page.goto('https://httpbin.org/cookies', { timeout: 60000 })).json()).cookies;
const draft = (page) => page.evaluate(() => localStorage.getItem('draft'));

// Session 1 (Chromium): "log in", then save cookies + localStorage to a local file.
const s1 = await launch('chromium');
const b1 = await chromium.connect(s1.wsUrl, { headers: { 'x-api-key': KEY } });
let saved;
try {
  const ctx = await b1.newContext();
  const page = await ctx.newPage();
  await page.goto('https://httpbin.org/cookies/set?session=abc123&user=alice', { timeout: 60000 });
  await page.evaluate(() => localStorage.setItem('draft', 'half-written review'));
  saved = await ctx.storageState();
  writeFileSync(STATE_FILE, JSON.stringify(saved, null, 2));
} finally {
  await b1.close(); // the browser is gone; only state.json remains
}

// Session 2 (Firefox, a fresh browser on whichever server the fleet picks): restore it.
const s2 = await launch('firefox');
const b2 = await firefox.connect(s2.wsUrl, { headers: { 'x-api-key': KEY } });
try {
  const restored = await b2.newContext({ storageState: JSON.parse(readFileSync(STATE_FILE, 'utf8')) });
  const page = await restored.newPage();
  const cookies = await cookiesSeen(page);
  const localStorageValue = await draft(page);

  // The same browser without the state, for comparison.
  const blank = await (await b2.newContext()).newPage();
  const blankCookies = await cookiesSeen(blank);

  console.log(JSON.stringify({
    session_1: { id: s1.sessionId, browser: 'chromium', saved_cookies: saved.cookies.map((c) => `${c.name}@${c.domain}`), saved_origins: saved.origins.map((o) => o.origin) },
    state_file_bytes: statSync(STATE_FILE).size,
    session_2: { id: s2.sessionId, browser: 'firefox', cookies_sent: cookies, local_storage_draft: localStorageValue },
    session_2_without_state: { cookies_sent: blankCookies },
  }, null, 2));
} finally {
  await b2.close();
}

What we got

StepBrowserCookies the site receivedlocalStorage draft
session 1: logged in, state savedchromium[email protected], [email protected](set)
session 2: restored from the filefirefox{"session":"abc123","user":"alice"}half-written review
session 2: new context, no statefirefox{}—

From the Node.js run on 2026-10-01. 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)
{
  "session_1": {
    "id": "4e47ea1d-9e10-424a-ab70-f248c3361978",
    "browser": "chromium",
    "saved_cookies": [
      "[email protected]",
      "[email protected]"
    ],
    "saved_origins": [
      "https://httpbin.org"
    ]
  },
  "state_file_bytes": 620,
  "session_2": {
    "id": "5fd28c8c-4463-4fb2-aae6-7988fc2692ab",
    "browser": "firefox",
    "cookies_sent": {
      "session": "abc123",
      "user": "alice"
    },
    "local_storage_draft": "half-written review"
  },
  "session_2_without_state": {
    "cookies_sent": {}
  }
}

Takeaways