> ## Documentation Index
> Fetch the complete documentation index at: https://kernel.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> Pre-configure pools of reserved browsers

Browser pools let you maintain a set of reserved, identical browsers ready for immediate use. Use them to set your preferred browser configuration in advance (such as stealth, proxies, extensions, and profiles), allowing you to minimize browser start-up latency and scale your workloads in production.

Acquiring a browser from a pool is faster than creating a browser directly. Reserved browsers and on-demand browsers share the same concurrency limit, and reserved browsers aren't billed until they're used. See [Scale](/docs/introduction/scale) for how pools fit into best practices for production architecture.

## How browser pools work

Browser pools are a way to pre-configure a fixed set of browsers without being charged for them until they are used (i.e. `acquired`). All browsers in the pool share the same settings upon instantiation.

<Steps>
  <Step title="Declare a pool">
    First, declare a pool of browsers with your specified configuration. The pool takes time to fill (see [fill rate per minute](https://www.kernel.sh/docs/api-reference/browser-pools/create-a-browser-pool#body-fill-rate-per-minute)), so declare your pool outside your browser automation / agent runtime logic.

    <Info>
      Pool declarations should be decoupled from browser runtime logic for the best performance.
    </Info>
  </Step>

  <Step title="Acquire a browser">
    You can acquire a browser as soon as you've created a pool. The request returns immediately if a browser is available, or waits until one becomes available.

    The total number of browsers is fixed to the `size` specified upon browser pool creation. When you acquire a browser, the pool's available count is decremented by one. When you release a browser, the pool's available count is incremented by one.

    <Info>
      Put differently, the pool does not top up when you acquire a browser: browsers are "borrowed" from the pool and must be returned when you're done with them, either by [releasing them](#release-a-browser) or allowing them to [timeout](#timeout-behavior).
    </Info>
  </Step>

  <Step title="Release a browser">
    When you're done with a browser, release it back to the pool. This step is important; otherwise, the browser will continue to be in an `acquired` state until it times out.

    <Info>
      Failing to release browsers may result in unexpected latency when acquiring future browsers.
    </Info>
  </Step>
</Steps>

## Create a pool of reserved browsers

Create a browser pool with a specified size and configuration. All browsers in the pool share the same settings.

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  import Kernel from '@onkernel/sdk';

  const kernel = new Kernel();

  const pool = await kernel.browserPools.create({
    name: "my-pool",
    size: 10,
    stealth: true,
    headless: false,
    timeout_seconds: 600,
    start_url: "https://example.com",
    viewport: {
      width: 1280,
      height: 800
    }
  });

  console.log(pool.id);
  ```

  ```python Python theme={null}
  from kernel import Kernel

  kernel = Kernel()

  pool = kernel.browser_pools.create(
      name="my-pool",
      size=10,
      stealth=True,
      headless=False,
      timeout_seconds=600,
      start_url="https://example.com",
      viewport={
          "width": 1280,
          "height": 800
      }
  )

  print(pool.id)
  ```

  ```go Go theme={null}
  package main

  import (
  	"context"
  	"fmt"

  	"github.com/kernel/kernel-go-sdk"
  	"github.com/kernel/kernel-go-sdk/shared"
  )

  func main() {
  	ctx := context.Background()
  	client := kernel.NewClient()

  	pool, err := client.BrowserPools.New(ctx, kernel.BrowserPoolNewParams{
  		Name:           kernel.String("my-pool"),
  		Size:           10,
  		Stealth:        kernel.Bool(true),
  		Headless:       kernel.Bool(false),
  		TimeoutSeconds: kernel.Int(600),
  		StartURL:       kernel.String("https://example.com"),
  		Viewport: shared.BrowserViewportParam{
  			Width:  1280,
  			Height: 800,
  		},
  	})
  	if err != nil {
  		panic(err)
  	}

  	fmt.Println(pool.ID)
  }
  ```
</CodeGroup>

### Pool configuration options

Pools can be pre-configured with options like start url, custom extensions, supported viewports, residential proxies, profiles, and more. See the [API reference](https://kernel.sh/docs/api-reference/browser-pools/create-a-browser-pool) for more details.

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 (it is silently ignored if sent). To capture profile state, use a single browser session with `save_changes` instead — see [Profiles](/docs/auth/profiles).

When a pool has a profile attached, `refresh_on_profile_update` is automatically enabled — the pool flushes idle browsers whenever the profile's contents are updated, so it always picks up the latest profile data. See [Refresh on profile update](#refresh-on-profile-update) below.

## Acquire a browser

Acquire a browser from the pool. The request returns immediately if a browser is available, or waits until one becomes available. The `acquire_timeout_seconds` parameter controls how long to wait; it defaults to the calculated time it would take to fill the pool at the pool's configured [fill rate](https://kernel.sh/docs/api-reference/browser-pools/create-a-browser-pool#body-fill-rate-per-minute).

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  const browser = await kernel.browserPools.acquire("my-pool", {
    acquire_timeout_seconds: 30,
  });

  console.log(browser.session_id);
  console.log(browser.cdp_ws_url);
  ```

  ```python Python theme={null}
  browser = kernel.browser_pools.acquire(
      "my-pool",
      acquire_timeout_seconds=30,
  )

  print(browser.session_id)
  print(browser.cdp_ws_url)
  ```

  ```go Go theme={null}
  browser, err := client.BrowserPools.Acquire(ctx, "my-pool", kernel.BrowserPoolAcquireParams{
  	AcquireTimeoutSeconds: kernel.Int(30),
  })
  if err != nil {
  	panic(err)
  }

  fmt.Println(browser.SessionID)
  fmt.Println(browser.CdpWsURL)
  ```
</CodeGroup>

The acquired browser includes all the same properties as a regular browser session, including `cdp_ws_url` for CDP connections and `browser_live_view_url` for live viewing.

### Timeout behavior

Browsers remain in the pool indefinitely until acquired. Once acquired, the pool's `timeout_seconds` applies just like a [regular browser timeout](/docs/browsers/termination#automatic-deletion-via-timeout)—if the browser is idle (no CDP or live view connection) for longer than the timeout, it is destroyed and **not** returned to the pool. The pool will automatically create a replacement browser at the pool's configured [fill rate](https://kernel.sh/docs/api-reference/browser-pools/create-a-browser-pool#body-fill-rate-per-minute).

## Release a browser

When you're done with a browser, release it back to the pool. By default, the browser instance is reused. Set `reuse: false` to destroy it and create a fresh one.

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  await kernel.browserPools.release("my-pool", {
    session_id: browser.session_id,
    reuse: true,
  });
  ```

  ```python Python theme={null}
  kernel.browser_pools.release(
      "my-pool",
      session_id=browser.session_id,
      reuse=True,
  )
  ```

  ```go Go theme={null}
  if err := client.BrowserPools.Release(ctx, "my-pool", kernel.BrowserPoolReleaseParams{
  	SessionID: browser.SessionID,
  	Reuse:     kernel.Bool(true),
  }); err != nil {
  	panic(err)
  }
  ```
</CodeGroup>

## Update a pool

Update the pool configuration. By default, existing idle browsers keep their current configuration and only newly created browsers use the new one. Pass `discard_all_idle: true` to discard all idle browsers and rebuild them immediately with the new configuration.

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  const updatedPool = await kernel.browserPools.update("my-pool", {
    size: 20,
    stealth: true,
  });
  ```

  ```python Python theme={null}
  updated_pool = kernel.browser_pools.update(
      "my-pool",
      size=20,
      stealth=True,
  )
  ```

  ```go Go theme={null}
  updatedPool, err := client.BrowserPools.Update(ctx, "my-pool", kernel.BrowserPoolUpdateParams{
  	Size:    20,
  	Stealth: kernel.Bool(true),
  })
  if err != nil {
  	panic(err)
  }
  _ = updatedPool
  ```
</CodeGroup>

<Info>
  The `size` parameter is always required when updating a pool, even if you only want to change other settings.
</Info>

By default (`discard_all_idle: false`), updating a pool leaves existing idle browsers untouched — they keep their original configuration, and only browsers created after the update use the new configuration. Set `discard_all_idle: true` to discard all idle browsers and rebuild them immediately with the new configuration.

## Refresh on profile update

When a pool is configured with a [profile](/docs/auth/profiles), the profile data is loaded into each browser when it's created. Normally, updating the profile's contents (for example, re-saving cookies or auth state to the same profile) does not propagate to already-warmed browsers — only newly-filled browsers use the updated profile.

`refresh_on_profile_update` is automatically set to `true` when a pool is created with a profile, or when an existing pool's profile is changed. This ensures that acquired browsers always get the latest profile data without manual intervention. You can explicitly set it to `false` to opt out.

When a pool's profile is removed (by passing `{ "id": "" }`), `refresh_on_profile_update` is forced to `false`.

<Info>
  `refresh_on_profile_update` requires a profile to be set on the pool. Setting it explicitly to `true` on a pool without a profile returns a validation error. When created without a profile, it defaults to `false`.
</Info>

### How it works

When a profile is saved (for example, after a successful [Managed Auth](/docs/auth/overview) login or a `save_changes` browser session), Kernel checks whether any browser pools use that profile with `refresh_on_profile_update` enabled. If so, all idle browsers in each matching pool are flushed — they are replaced with fresh browsers that load the updated profile. Acquired browsers are not affected; they continue running with the profile data they were created with.

<Warning>
  Reused browsers keep the configuration they were created with. A plain `update()` rebuilds nothing that already exists — it only changes the config used for future browsers. To rebuild the idle browsers too, pass `discard_all_idle: true` (or call [`flush()`](#flush-idle-browsers)). Either way, browsers that are acquired during an update are never touched: an in-use browser keeps its original configuration, and if you then release it with `reuse: true` (the default) it re-enters the pool still carrying that stale configuration and keeps getting handed out that way.

  You have three ways to get an in-use browser onto the new configuration:

  * **Prevent it on release:** release with `reuse: false`. The browser is destroyed and rebuilt with the current pool configuration instead of the old one returning to the pool.
  * **Let it expire:** don't release the acquired browser for reuse. Let it reach its `timeout_seconds` while idle, at which point it's destroyed and the pool refills automatically with 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 theme={null}
  // 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 theme={null}
  # 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 theme={null}
  // 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.

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  await kernel.browserPools.flush("my-pool");
  ```

  ```python Python theme={null}
  kernel.browser_pools.flush("my-pool")
  ```

  ```go Go theme={null}
  if err := client.BrowserPools.Flush(ctx, "my-pool"); err != nil {
  	panic(err)
  }
  ```
</CodeGroup>

## Get pool details

Retrieve the current status and configuration of a pool.

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  const pool = await kernel.browserPools.retrieve("my-pool");

  console.log(pool.available_count);
  console.log(pool.acquired_count);
  ```

  ```python Python theme={null}
  pool = kernel.browser_pools.retrieve("my-pool")

  print(pool.available_count)
  print(pool.acquired_count)
  ```

  ```go Go theme={null}
  pool, err := client.BrowserPools.Get(ctx, "my-pool")
  if err != nil {
  	panic(err)
  }

  fmt.Println(pool.AvailableCount)
  fmt.Println(pool.AcquiredCount)
  ```
</CodeGroup>

## List pools

List all browser pools in your organization.

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  const pools = await kernel.browserPools.list();

  for (const pool of pools) {
    console.log(pool.name, pool.available_count);
  }
  ```

  ```python Python theme={null}
  pools = kernel.browser_pools.list()

  for pool in pools:
      print(pool.name, pool.available_count)
  ```

  ```go Go theme={null}
  pools, err := client.BrowserPools.List(ctx)
  if err != nil {
  	panic(err)
  }

  for _, pool := range *pools {
  	fmt.Println(pool.Name, pool.AvailableCount)
  }
  ```
</CodeGroup>

## Delete a pool

Delete a browser pool and all browsers in it. By default, deletion is blocked if browsers are currently acquired. Use `force: true` to terminate acquired browsers and force deletion.

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  // Delete a pool (fails if browsers are acquired)
  await kernel.browserPools.delete("my-pool");

  // Force delete even if browsers are acquired
  await kernel.browserPools.delete("my-pool", { force: true });
  ```

  ```python Python theme={null}
  # Delete a pool (fails if browsers are acquired)
  kernel.browser_pools.delete("my-pool")

  # Force delete even if browsers are acquired
  kernel.browser_pools.delete("my-pool", force=True)
  ```

  ```go Go theme={null}
  // Delete a pool (fails if browsers are acquired)
  if err := client.BrowserPools.Delete(ctx, "my-pool", kernel.BrowserPoolDeleteParams{}); err != nil {
  	panic(err)
  }

  // Force delete even if browsers are acquired
  if err := client.BrowserPools.Delete(ctx, "my-pool", kernel.BrowserPoolDeleteParams{
  	Force: kernel.Bool(true),
  }); err != nil {
  	panic(err)
  }
  ```
</CodeGroup>

## Full example

This example assumes you've already created a pool named "my-pool". In practice, you'd create pools once (via the SDK, CLI, or dashboard) and then acquire from them repeatedly.

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  import Kernel from '@onkernel/sdk';
  import { chromium } from 'playwright';

  const kernel = new Kernel();

  // Acquire a browser from an existing pool
  const kernelBrowser = await kernel.browserPools.acquire("my-pool", {});

  try {
    // Connect via CDP
    const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
    const context = browser.contexts()[0];
    const page = context.pages()[0];

    await page.goto('https://example.com');
    const title = await page.title();
    console.log(title);
  } finally {
    // Release back to pool for reuse
    await kernel.browserPools.release("my-pool", {
      session_id: kernelBrowser.session_id,
    });
  }
  ```

  ```python Python theme={null}
  import asyncio
  from kernel import Kernel
  from playwright.async_api import async_playwright

  kernel = Kernel()

  async def main():
      # Acquire a browser from an existing pool
      kernel_browser = kernel.browser_pools.acquire("my-pool")

      async with async_playwright() as playwright:
          try:
              # Connect via CDP
              browser = await playwright.chromium.connect_over_cdp(kernel_browser.cdp_ws_url)
              context = browser.contexts[0]
              page = context.pages[0]

              await page.goto('https://example.com')
              title = await page.title()
              print(title)
          finally:
              # Release back to pool for reuse
              kernel.browser_pools.release(
                  "my-pool",
                  session_id=kernel_browser.session_id,
              )

  asyncio.run(main())
  ```

  ```go Go theme={null}
  package main

  import (
  	"context"
  	"fmt"

  	"github.com/kernel/kernel-go-sdk"
  )

  func main() {
  	ctx := context.Background()
  	client := kernel.NewClient()

  	// Acquire a browser from an existing pool
  	kernelBrowser, err := client.BrowserPools.Acquire(ctx, "my-pool", kernel.BrowserPoolAcquireParams{})
  	if err != nil {
  		panic(err)
  	}

  	defer func() {
  		// Release back to pool for reuse
  		if err := client.BrowserPools.Release(ctx, "my-pool", kernel.BrowserPoolReleaseParams{
  			SessionID: kernelBrowser.SessionID,
  		}); err != nil {
  			panic(err)
  		}
  	}()

  	response, err := client.Browsers.Playwright.Execute(ctx, kernelBrowser.SessionID, kernel.BrowserPlaywrightExecuteParams{
  		Code: `
  			await page.goto('https://example.com');
  			return await page.title();
  		`,
  	})
  	if err != nil {
  		panic(err)
  	}

  	fmt.Println(response.Result)
  }
  ```
</CodeGroup>

## API reference

For more details on all available endpoints and parameters, see the [Browser Pools API reference](https://kernel.sh/docs/api-reference/browser-pools/list-browser-pools).
