diff --git a/auth/profiles.mdx b/auth/profiles.mdx
index 4f47c72..5bb0cf4 100644
--- a/auth/profiles.mdx
+++ b/auth/profiles.mdx
@@ -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.
+
+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)
+
+
## 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.
@@ -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.
diff --git a/browsers/pools/faq.mdx b/browsers/pools/faq.mdx
index e78e791..b53637e 100644
--- a/browsers/pools/faq.mdx
+++ b/browsers/pools/faq.mdx
@@ -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?
diff --git a/browsers/pools/overview.mdx b/browsers/pools/overview.mdx
index 0fc3d6a..6ebcbf6 100644
--- a/browsers/pools/overview.mdx
+++ b/browsers/pools/overview.mdx
@@ -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.
+## 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.
+
+
+```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)
+}
+```
+
+
+
+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.
+
+
## 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.