From 8e7d911d55275f85b09d9656011e63ba4f0f092d Mon Sep 17 00:00:00 2001 From: AnnaXWang <6621137+AnnaXWang@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:42:46 +0000 Subject: [PATCH] Document save_changes requirement for per-user pool profiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-user browser pool flow said destroying the browser persists the user's profile changes, but save_changes defaults to false, so following the docs verbatim discards every user's session on release. Spell out the opt-in and add a TS/Python/Go example of the full acquire-attach-release cycle. Also scope the "profiles load read-only" limitation to the pool's own profile — a profile attached after acquiring is writable — and point that bullet at the per-user section rather than its parent, which describes the read-only case it is contrasting against. --- auth/profiles.mdx | 2 +- browsers/pools.mdx | 65 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/auth/profiles.mdx b/auth/profiles.mdx index a37a81f..381bf00 100644 --- a/auth/profiles.mdx +++ b/auth/profiles.mdx @@ -514,5 +514,5 @@ _ = browser - Profiles store cookies and local storage. Start the session with `save_changes: true` to write changes back when the browser is closed. - To keep a profile immutable for a run, omit `save_changes` (default) when creating the browser. - Multiple browsers in parallel can use the same profile, but only one browser should write (`save_changes: true`) to it at a time. Parallel browsers with `save_changes: true` may cause profile corruption and unpredictable behavior. -- `save_changes` applies to a profile attached to a single browser — either at creation (`kernel.browsers.create()`) or loaded afterward with `kernel.browsers.update()`. A profile set on a [browser pool's](/browsers/pools) config is loaded read-only and never persisted; `save_changes` sent on a pool's profile is silently ignored. To persist per-user state through a pool, attach the profile after acquiring the browser and release with `reuse: false` — see [Per-user profiles with pools](/browsers/pools#per-user-profiles-with-browser-pools). +- `save_changes` applies to a profile attached to a single browser — either at creation (`kernel.browsers.create()`) or loaded afterward with `kernel.browsers.update()`. A profile set on a [browser pool's](/browsers/pools) config is loaded read-only and never persisted; `save_changes` sent on a pool's profile is silently ignored. To persist per-user state through a pool, attach the profile with `save_changes: true` after acquiring the browser and release with `reuse: false` — see [Per-user profiles with pools](/browsers/pools#per-user-profiles-with-browser-pools). - Profile data is encrypted end to end using a per-organization key. diff --git a/browsers/pools.mdx b/browsers/pools.mdx index 526b402..0eace54 100644 --- a/browsers/pools.mdx +++ b/browsers/pools.mdx @@ -29,7 +29,7 @@ A few constraints to weigh before moving a workload onto a browser pool: - **No GPU browsers.** GPU-accelerated browsers are on-demand only. Use `browsers.create()` for WebGL, video, or canvas-heavy work. - **One fixed configuration per browser pool**, with `start_url` the only setting you can override per acquisition — see [Create a browser pool](#create-a-browser-pool). -- **Profiles load read-only**, and a browser pool holds one at a time — see [Profiles with browser pools](#profiles-with-browser-pools) for how to persist state per user. +- **A profile set on the browser pool loads read-only**, and a browser pool holds one at a time — see [Per-user profiles with browser pools](#per-user-profiles-with-browser-pools) for how to persist state per user. - **Browser pool capacity counts against your [concurrency limit](/info/pricing#concurrency-limits)** whether or not its browsers are acquired, though idle pooled browsers aren't billed. - **Plan-gated.** Browser pools are available on the Start-Up and Enterprise plans. @@ -208,7 +208,68 @@ A profile attached to the pool is loaded **read-only**. Every browser in the poo ### Per-user profiles with browser pools -Because that profile is shared and read-only, it can't hold per-user login state for many users at once. To serve many users from one browser pool, create it with no profile — stealth, proxies, extensions, and viewport still live on the pool — then attach each user's profile to the browser *after* you acquire it, and release with `reuse: false` so the browser is destroyed. Destroying it both persists that user's profile changes and keeps their state from reaching the next acquirer. +Because that profile is shared and read-only, it can't hold per-user login state for many users at once. To serve many users from one browser pool, create it with no profile — stealth, proxies, extensions, and viewport still live on the pool — then attach each user's profile to the browser *after* you acquire it, and release with `reuse: false` so the browser is destroyed. Destroying it keeps that user's state from reaching the next acquirer. + +The read-only rule covers the pool's own profile, not one attached after acquiring: that profile belongs to the browser, so `save_changes` applies as it does on any other browser. Pass `save_changes: true` when you attach it — it defaults to `false`, and without it the browser is destroyed on release without writing the user's session back. + + +```typescript Typescript/Javascript +const browser = await kernel.browserPools.acquire("my-pool"); + +await kernel.browsers.update(browser.session_id, { + profile: { name: "user-8f21c3", save_changes: true } +}); + +// ... drive the browser as that user ... + +await kernel.browserPools.release("my-pool", { + session_id: browser.session_id, + reuse: false, +}); +``` + +```python Python +browser = kernel.browser_pools.acquire("my-pool") + +kernel.browsers.update( + browser.session_id, + profile={"name": "user-8f21c3", "save_changes": True}, +) + +# ... drive the browser as that user ... + +kernel.browser_pools.release( + "my-pool", + session_id=browser.session_id, + reuse=False, +) +``` + +```go Go +browser, err := client.BrowserPools.Acquire(ctx, "my-pool", kernel.BrowserPoolAcquireParams{}) +if err != nil { + panic(err) +} + +if _, err := client.Browsers.Update(ctx, browser.SessionID, kernel.BrowserUpdateParams{ + Profile: shared.BrowserProfileParam{ + Name: kernel.String("user-8f21c3"), + SaveChanges: kernel.Bool(true), + }, +}); err != nil { + panic(err) +} + +// ... drive the browser as that user ... + +if err := client.BrowserPools.Release(ctx, "my-pool", kernel.BrowserPoolReleaseParams{ + SessionID: browser.SessionID, + Reuse: kernel.Bool(false), +}); err != nil { + panic(err) +} +``` + A profile can only be loaded into a browser that was created without one, which is why the pool itself has to stay profile-free.