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.
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
| What | Why |
|---|---|
context.storageState() | Exports every cookie and each origin's localStorage as JSON — Playwright's portable "logged-in state". |
| A file on your machine | The 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 firefox | Two different engines on purpose: the state format is engine-independent. |
| httpbin.org cookies + localStorage | Stand-ins for a real site's session cookie and saved draft. |
How it works
- Session 1 (Chromium): set two cookies and a localStorage value, save the state to a file, close the browser.
- Session 2 (Firefox, a new browser on whichever server the fleet picks): open a context from the file and check what the site receives.
- 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:
CDPFLEET_API_KEY— your API key (dashboard → API keys)PROXY_URL— your proxy, e.g.http://user:[email protected]:8000
// 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();
}
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL
import json
import os
import tempfile
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
STATE_FILE = os.path.join(tempfile.gettempdir(), "cdpfleet-state.json") # keep it safe: it holds the login
def launch(name):
res = requests.post(f"https://starter.cdpfleet.com/{name}/session", headers={"x-api-key": KEY},
json={"proxy": os.environ["PROXY_URL"], "headless": True}, timeout=60)
res.raise_for_status()
return res.json()
def cookies_seen(page):
return page.goto("https://httpbin.org/cookies", timeout=60000).json()["cookies"]
with sync_playwright() as p:
# Session 1 (Chromium): "log in", then save cookies + localStorage to a local file.
s1 = launch("chromium")
b1 = p.chromium.connect(s1["wsUrl"], headers={"x-api-key": KEY})
try:
ctx = b1.new_context()
page = ctx.new_page()
page.goto("https://httpbin.org/cookies/set?session=abc123&user=alice", timeout=60000)
page.evaluate("localStorage.setItem('draft', 'half-written review')")
saved = ctx.storage_state(path=STATE_FILE)
finally:
b1.close() # the browser is gone; only the state file remains
# Session 2 (Firefox, a fresh browser on whichever server the fleet picks): restore it.
s2 = launch("firefox")
b2 = p.firefox.connect(s2["wsUrl"], headers={"x-api-key": KEY})
try:
page = b2.new_context(storage_state=STATE_FILE).new_page()
cookies = cookies_seen(page)
draft = page.evaluate("localStorage.getItem('draft')")
blank_cookies = cookies_seen(b2.new_context().new_page()) # the same browser without the state
print(json.dumps({
"session_1": {"id": s1["sessionId"], "browser": "chromium",
"saved_cookies": [f"{c['name']}@{c['domain']}" for c in saved["cookies"]],
"saved_origins": [o["origin"] for o in saved["origins"]]},
"state_file_bytes": os.path.getsize(STATE_FILE),
"session_2": {"id": s2["sessionId"], "browser": "firefox", "cookies_sent": cookies, "local_storage_draft": draft},
"session_2_without_state": {"cookies_sent": blank_cookies},
}, indent=2))
finally:
b2.close()
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY, PROXY_URL
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.util.Map;
public class Main {
static final String KEY = System.getenv("CDPFLEET_API_KEY");
// Keep it somewhere safe: it holds the login.
static final Path STATE_FILE = Paths.get(System.getProperty("java.io.tmpdir"), "cdpfleet-state.json");
static JsonObject launch(String name) throws Exception {
String body = "{\"proxy\": " + new Gson().toJson(System.getenv("PROXY_URL")) + ", \"headless\": true}";
HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/" + name + "/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 200) throw new RuntimeException("launch " + name + ": " + res.statusCode() + " " + res.body());
return JsonParser.parseString(res.body()).getAsJsonObject();
}
static JsonElement cookiesSeen(Page page) {
return JsonParser.parseString(page.navigate("https://httpbin.org/cookies", new Page.NavigateOptions().setTimeout(60000)).text())
.getAsJsonObject().get("cookies");
}
public static void main(String[] args) throws Exception {
try (Playwright playwright = Playwright.create()) {
Map<String, String> headers = Map.of("x-api-key", KEY);
// Session 1 (Chromium): "log in", then save cookies + localStorage to a local file.
JsonObject s1 = launch("chromium");
Browser b1 = playwright.chromium().connect(s1.get("wsUrl").getAsString(), new BrowserType.ConnectOptions().setHeaders(headers));
JsonObject saved;
try {
BrowserContext ctx = b1.newContext();
Page page = ctx.newPage();
page.navigate("https://httpbin.org/cookies/set?session=abc123&user=alice", new Page.NavigateOptions().setTimeout(60000));
page.evaluate("localStorage.setItem('draft', 'half-written review')");
saved = JsonParser.parseString(ctx.storageState(new BrowserContext.StorageStateOptions().setPath(STATE_FILE))).getAsJsonObject();
} finally {
b1.close(); // the browser is gone; only the state file remains
}
// Session 2 (Firefox, a fresh browser on whichever server the fleet picks): restore it.
JsonObject s2 = launch("firefox");
Browser b2 = playwright.firefox().connect(s2.get("wsUrl").getAsString(), new BrowserType.ConnectOptions().setHeaders(headers));
try {
Page page = b2.newContext(new Browser.NewContextOptions().setStorageStatePath(STATE_FILE)).newPage();
JsonElement cookies = cookiesSeen(page);
Object draft = page.evaluate("localStorage.getItem('draft')");
JsonElement blankCookies = cookiesSeen(b2.newContext().newPage()); // the same browser without the state
JsonArray savedCookies = new JsonArray();
for (JsonElement c : saved.getAsJsonArray("cookies")) {
savedCookies.add(c.getAsJsonObject().get("name").getAsString() + "@" + c.getAsJsonObject().get("domain").getAsString());
}
JsonArray origins = new JsonArray();
for (JsonElement o : saved.getAsJsonArray("origins")) origins.add(o.getAsJsonObject().get("origin"));
JsonObject out = new JsonObject();
JsonObject one = new JsonObject();
one.add("id", s1.get("sessionId"));
one.addProperty("browser", "chromium");
one.add("saved_cookies", savedCookies);
one.add("saved_origins", origins);
out.add("session_1", one);
out.addProperty("state_file_bytes", Files.size(STATE_FILE));
JsonObject two = new JsonObject();
two.add("id", s2.get("sessionId"));
two.addProperty("browser", "firefox");
two.add("cookies_sent", cookies);
two.addProperty("local_storage_draft", (String) draft);
out.add("session_2", two);
JsonObject blank = new JsonObject();
blank.add("cookies_sent", blankCookies);
out.add("session_2_without_state", blank);
System.out.println(new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(out));
} finally {
b2.close();
}
}
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
var stateFile = Path.Combine(Path.GetTempPath(), "cdpfleet-state.json"); // keep it safe: it holds the login
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
using var playwright = await Playwright.CreateAsync();
var headers = new Dictionary<string, string> { ["x-api-key"] = key };
async Task<JsonElement> Launch(string name)
{
var res = await http.PostAsJsonAsync($"https://starter.cdpfleet.com/{name}/session",
new { proxy = Environment.GetEnvironmentVariable("PROXY_URL"), headless = true });
if (!res.IsSuccessStatusCode) throw new Exception($"launch {name}: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
return await res.Content.ReadFromJsonAsync<JsonElement>();
}
async Task<JsonNode> CookiesSeen(IPage page) =>
JsonNode.Parse(await (await page.GotoAsync("https://httpbin.org/cookies", new() { Timeout = 60000 }))!.TextAsync())!["cookies"]!.DeepClone();
// Session 1 (Chromium): "log in", then save cookies + localStorage to a local file.
var s1 = await Launch("chromium");
var b1 = await playwright.Chromium.ConnectAsync(s1.GetProperty("wsUrl").GetString()!, new() { Headers = headers });
JsonNode saved;
try
{
var ctx = await b1.NewContextAsync();
var page = await ctx.NewPageAsync();
await page.GotoAsync("https://httpbin.org/cookies/set?session=abc123&user=alice", new() { Timeout = 60000 });
await page.EvaluateAsync("localStorage.setItem('draft', 'half-written review')");
saved = JsonNode.Parse(await ctx.StorageStateAsync(new() { Path = stateFile }))!;
}
finally
{
await b1.CloseAsync(); // the browser is gone; only the state file remains
}
// Session 2 (Firefox, a fresh browser on whichever server the fleet picks): restore it.
var s2 = await Launch("firefox");
var b2 = await playwright.Firefox.ConnectAsync(s2.GetProperty("wsUrl").GetString()!, new() { Headers = headers });
try
{
var page = await (await b2.NewContextAsync(new() { StorageStatePath = stateFile })).NewPageAsync();
var cookies = await CookiesSeen(page);
var draft = await page.EvaluateAsync<string>("localStorage.getItem('draft')");
var blankCookies = await CookiesSeen(await (await b2.NewContextAsync()).NewPageAsync()); // the same browser without the state
var result = new JsonObject
{
["session_1"] = new JsonObject
{
["id"] = s1.GetProperty("sessionId").GetString(),
["browser"] = "chromium",
["saved_cookies"] = new JsonArray(saved["cookies"]!.AsArray().Select(c => (JsonNode?)$"{c!["name"]}@{c["domain"]}").ToArray()),
["saved_origins"] = new JsonArray(saved["origins"]!.AsArray().Select(o => (JsonNode?)(string)o!["origin"]!).ToArray()),
},
["state_file_bytes"] = new FileInfo(stateFile).Length,
["session_2"] = new JsonObject
{
["id"] = s2.GetProperty("sessionId").GetString(), ["browser"] = "firefox", ["cookies_sent"] = cookies, ["local_storage_draft"] = draft,
},
["session_2_without_state"] = new JsonObject { ["cookies_sent"] = blankCookies },
};
Console.WriteLine(result.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
}
finally
{
await b2.CloseAsync();
}
// go get github.com/playwright-community/[email protected]
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"github.com/playwright-community/playwright-go"
)
var key = os.Getenv("CDPFLEET_API_KEY")
// Keep it somewhere safe: it holds the login.
var stateFile = filepath.Join(os.TempDir(), "cdpfleet-state.json")
func launch(name string, options map[string]any) (map[string]any, error) {
body, _ := json.Marshal(options)
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
}
var session map[string]any
return session, json.NewDecoder(res.Body).Decode(&session)
}
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func cookiesSeen(page playwright.Page) any {
res := must(page.Goto("https://httpbin.org/cookies", playwright.PageGotoOptions{Timeout: playwright.Float(60000)}))
var v map[string]any
must(0, res.JSON(&v))
return v["cookies"]
}
func main() {
pw := must(playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true}))
defer pw.Stop()
headers := playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}}
opts := map[string]any{"proxy": os.Getenv("PROXY_URL"), "headless": true}
// Session 1 (Chromium): "log in", then save cookies + localStorage to a local file.
s1 := must(launch("chromium", opts))
b1 := must(pw.Chromium.Connect(s1["wsUrl"].(string), headers))
ctx := must(b1.NewContext())
page := must(ctx.NewPage())
must(page.Goto("https://httpbin.org/cookies/set?session=abc123&user=alice", playwright.PageGotoOptions{Timeout: playwright.Float(60000)}))
must(page.Evaluate("localStorage.setItem('draft', 'half-written review')"))
must(ctx.StorageState(playwright.BrowserContextStorageStateOptions{Path: playwright.String(stateFile)}))
b1.Close() // the browser is gone; only the state file remains
var saved struct {
Cookies []struct{ Name, Domain string } `json:"cookies"`
Origins []struct{ Origin string } `json:"origins"`
}
raw := must(os.ReadFile(stateFile))
must(0, json.Unmarshal(raw, &saved))
savedCookies, origins := []string{}, []string{}
for _, c := range saved.Cookies {
savedCookies = append(savedCookies, c.Name+"@"+c.Domain)
}
for _, o := range saved.Origins {
origins = append(origins, o.Origin)
}
// Session 2 (Firefox, a fresh browser on whichever server the fleet picks): restore it.
s2 := must(launch("firefox", opts))
b2 := must(pw.Firefox.Connect(s2["wsUrl"].(string), headers))
defer b2.Close()
restored := must(b2.NewContext(playwright.BrowserNewContextOptions{StorageStatePath: playwright.String(stateFile)}))
page2 := must(restored.NewPage())
cookies := cookiesSeen(page2)
draft := must(page2.Evaluate("localStorage.getItem('draft')"))
blank := must(must(b2.NewContext()).NewPage()) // the same browser without the state
out, _ := json.MarshalIndent(map[string]any{
"session_1": map[string]any{"id": s1["sessionId"], "browser": "chromium", "saved_cookies": savedCookies, "saved_origins": origins},
"state_file_bytes": len(raw),
"session_2": map[string]any{"id": s2["sessionId"], "browser": "firefox", "cookies_sent": cookies, "local_storage_draft": draft},
"session_2_without_state": map[string]any{"cookies_sent": cookiesSeen(blank)},
}, "", " ")
fmt.Println(string(out))
}
What we got
| Step | Browser | Cookies the site received | localStorage draft |
|---|---|---|---|
| session 1: logged in, state saved | chromium | [email protected], [email protected] | (set) |
| session 2: restored from the file | firefox | {"session":"abc123","user":"alice"} | half-written review |
| session 2: new context, no state | firefox | {} | — |
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
- The restored Firefox context sent both cookies and still had the localStorage draft — a site would see the same logged-in user — while a context without the state sent nothing.
- It works across engines and servers: the state is plain JSON, a few hundred bytes here, not tied to the browser that made it.
- Refresh it as you go: sites rotate session cookies, so save the state again at the end of each session.
- Keep the fingerprint consistent: some sites tie a login to the device that made it. Restore the state into the same browser family, OS and country you logged in with if logins get challenged.