Skip to content
6 changes: 5 additions & 1 deletion auth/profiles.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ if err != nil {
You cannot load a profile into a browser that was already created with a profile. The browser must have been created without any profile configuration.
</Warning>

<Note>
To use profiles with browser pools, read: [Can pooled browsers save changes back to a profile?](/browsers/pools/faq#can-pooled-browsers-save-changes-back-to-a-profile)
</Note>
Comment thread
cursor[bot] marked this conversation as resolved.

## Other ways to use profiles

The API and SDKs support listing, deleting, and downloading profile data as JSON. See the [API reference](https://kernel.sh/docs/api-reference/profiles/list-profiles) for more details.
Expand Down Expand Up @@ -484,5 +488,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 only to single browser sessions (`kernel.browsers.create()`). [Browser pools](/browsers/pools/overview) load a profile read-only and never persist changes back to it; `save_changes` sent on a pool's profile is silently ignored.
- `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/overview) 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/overview#per-user-profiles-with-pools).
- Profile data is encrypted end to end using a per-organization key.
7 changes: 6 additions & 1 deletion browsers/pools/faq.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ If you've explicitly disabled `refresh_on_profile_update`, you can still force t

### Can pooled browsers save changes back to a profile?

No. A profile attached to a pool is loaded read-only — pooled browsers never persist changes back to the profile, so `save_changes` does not apply to pools. Sending `save_changes` on a pool's profile is silently ignored (it is not rejected), so reusing a single-session profile object won't error. To capture profile state, use a single browser session instead: `kernel.browsers.create({ profile: { name, save_changes: true } })`.
Not at the pool level, but yes per session. These are two different things:

- **A profile set on the pool config** is loaded read-only. Every browser in the pool shares it and would try to save concurrently, so `save_changes` does not apply to pools — sending it on a pool's profile is silently ignored (not rejected). This is by design: it prevents profile corruption and the ambiguity of which idle browser's state would "win".
- **A profile attached to a single browser _after_ you acquire it** does persist. Create the pool with no profile, acquire a browser, attach a per-user profile with `save_changes: true` via `kernel.browsers.update()`, then release with `reuse: false` so the browser is destroyed (which flushes the profile) instead of returning to the pool.

The second pattern is the recommended way to load many per-user profiles through a pool's pre-warmed browsers — see [Per-user profiles with pools](/browsers/pools/overview#per-user-profiles-with-pools).

### Should I set `reuse: true` or `reuse: false` when releasing?

Expand Down
88 changes: 88 additions & 0 deletions browsers/pools/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,94 @@ You have three ways to get an in-use browser onto the new configuration:
- **Clean it up after the fact:** [`flush()`](#flush-idle-browsers) the pool, or run a later `update()` with `discard_all_idle: true`, once the in-use browsers have been released.
</Warning>

## Per-user profiles with pools

A profile attached to the pool config is [read-only](#create-a-pool-of-reserved-browsers) and shared by every browser, so it can't hold per-user login state across many users in a single pool. To use a pool's pre-warmed browsers to load many different per-user profiles **and** persist each user's state, attach the profile to the browser *after* you acquire it, then destroy the browser on release:

1. **Create the pool with no profile.** A profile can only be loaded into a browser that was created without one, so the pool must be profile-free.
2. **Acquire a browser** from the pool.
3. **Attach the user's profile** with `save_changes: true` using `kernel.browsers.update()`.
4. **Run your automation, then release with `reuse: false`.** This destroys the browser instead of returning it to the pool — which both persists the profile changes and prevents state from leaking to the next user.

<CodeGroup>
```typescript Typescript/Javascript
// Declare the pool once, separately from your workload runtime, with no profile attached
const pool = await kernel.browserPools.create({ name: "my-pool", size: 10 });

// Later, at runtime, acquire from the pool per user:
const browser = await kernel.browserPools.acquire("my-pool", {});

// Load this user's profile and persist any changes on destroy
await kernel.browsers.update(browser.session_id, {
profile: { name: userId, save_changes: true },
});

// ... run your automation against browser.cdp_ws_url ...

// Destroy on release so the profile persists and no state leaks to the next user
await kernel.browserPools.release("my-pool", {
session_id: browser.session_id,
reuse: false,
});
```

```python Python
# Declare the pool once, separately from your workload runtime, with no profile attached
pool = kernel.browser_pools.create(name="my-pool", size=10)

# Later, at runtime, acquire from the pool per user:
browser = kernel.browser_pools.acquire("my-pool")

# Load this user's profile and persist any changes on destroy
kernel.browsers.update(
browser.session_id,
profile={"name": user_id, "save_changes": True},
)

# ... run your automation against browser.cdp_ws_url ...

# Destroy on release so the profile persists and no state leaks to the next user
kernel.browser_pools.release(
"my-pool",
session_id=browser.session_id,
reuse=False,
)
```

```go Go
// Declare the pool once, separately from your workload runtime, with no profile attached.
// Later, at runtime, acquire from the pool per user:
browser, err := client.BrowserPools.Acquire(ctx, "my-pool", kernel.BrowserPoolAcquireParams{})
if err != nil {
panic(err)
}

// Load this user's profile and persist any changes on destroy
if _, err := client.Browsers.Update(ctx, browser.SessionID, kernel.BrowserUpdateParams{
Profile: shared.BrowserProfileParam{
Name: kernel.String(userID),
SaveChanges: kernel.Bool(true),
},
}); err != nil {
panic(err)
}

// ... run your automation against browser.CdpWsURL ...

// Destroy on release so the profile persists and no state leaks to the next user
if err := client.BrowserPools.Release(ctx, "my-pool", kernel.BrowserPoolReleaseParams{
SessionID: browser.SessionID,
Reuse: kernel.Bool(false),
}); err != nil {
panic(err)
}
```
</CodeGroup>

<Info>
Releasing with `reuse: false` triggers a pool refill at the configured [fill rate](https://kernel.sh/docs/api-reference/browser-pools/create-a-browser-pool#body-fill-rate-per-minute), so the pool tops back up to `size` with fresh, profile-free browsers ready for the next user.
</Info>

## Flush idle browsers

Destroy all idle browsers in the pool. Acquired browsers are not affected. The pool will automatically refill with the pool's specified configuration.
Expand Down
Loading