> ## Documentation Index
> Fetch the complete documentation index at: https://tbd-6fc993ce-hypeship-pool-sizing-calculator.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> Pre-configure pools of reserved browsers for immediate acquisition

export const PoolSizingCalculator = () => {
  const defaults = {
    tasksPerHour: 600,
    sessionDurationMinutes: 5,
    burstMode: 'steady',
    fillRate: 25
  };
  const [tasksPerHour, setTasksPerHour] = useState(defaults.tasksPerHour);
  const [sessionDurationMinutes, setSessionDurationMinutes] = useState(defaults.sessionDurationMinutes);
  const [burstMode, setBurstMode] = useState(defaults.burstMode);
  const [fillRate, setFillRate] = useState(defaults.fillRate);
  const [showAdvanced, setShowAdvanced] = useState(false);
  const [flash, setFlash] = useState(false);
  const prevResultRef = useRef(null);
  const hasInteracted = useRef(false);
  useEffect(() => {
    if (!hasInteracted.current) return;
    var url = new URL(window.location);
    url.searchParams.set('tasksPerHour', tasksPerHour);
    url.searchParams.set('sessionDuration', sessionDurationMinutes);
    url.searchParams.set('burstMode', burstMode);
    url.searchParams.set('fillRate', fillRate);
    url.hash = 'pool-sizing-calculator';
    window.history.replaceState(null, '', url);
  }, [tasksPerHour, sessionDurationMinutes, burstMode, fillRate]);
  const safety = 1.25;
  const burstMultiplier = burstMode === 'bursty' ? 2 : 1;
  const tasks = Number.isFinite(tasksPerHour) && tasksPerHour > 0 ? tasksPerHour : 0;
  const duration = Number.isFinite(sessionDurationMinutes) && sessionDurationMinutes > 0 ? sessionDurationMinutes : 0;
  const rate = Number.isFinite(fillRate) && fillRate > 0 ? Math.min(fillRate, 25) : 1;
  const lambda = tasks / 60 * burstMultiplier;
  const refillFloor = Math.ceil(100 * lambda / rate);
  const concurrencyFloor = Math.ceil(lambda * duration * safety);
  const poolSize = Math.max(refillFloor, concurrencyFloor);
  const bindingConstraint = concurrencyFloor >= refillFloor ? 'concurrency' : 'refill';
  useEffect(() => {
    var prev = prevResultRef.current;
    if (prev !== null && prev.poolSize !== poolSize) {
      setFlash(true);
      var t = setTimeout(() => setFlash(false), 300);
      return () => clearTimeout(t);
    }
    prevResultRef.current = {
      poolSize
    };
  }, [poolSize]);
  const labelStyle = {
    fontWeight: 600,
    fontSize: '0.875rem',
    minWidth: '10rem',
    flexShrink: 0,
    maxWidth: '10rem'
  };
  const rowStyle = {
    display: 'flex',
    alignItems: 'center',
    gap: '0.5rem',
    minHeight: '2.25rem',
    flexWrap: 'wrap'
  };
  const inputStyle = {
    minWidth: 0,
    flex: 1,
    maxWidth: '100%',
    boxSizing: 'border-box',
    background: 'transparent'
  };
  const numberInputStyle = {
    borderBottom: '1px solid #81b300',
    textAlign: 'right'
  };
  const flashStyle = {
    background: flash ? '#81b300' : 'transparent',
    transition: 'background 0.5s ease',
    marginLeft: 'auto'
  };
  const btnStyle = active => ({
    padding: '0.25rem 0.5rem',
    borderRadius: '0.375rem',
    border: `1px solid ${active ? '#81b300' : 'var(--btn-border)'}`,
    fontSize: '0.875rem',
    background: active ? 'var(--btn-selected-bg)' : undefined
  });
  const disclosureStyle = {
    background: 'none',
    border: 'none',
    padding: 0,
    fontSize: '0.8rem',
    color: '#81b300',
    cursor: 'pointer',
    textAlign: 'left'
  };
  const setRate = v => {
    hasInteracted.current = true;
    const n = parseInt(v);
    if (Number.isNaN(n)) {
      setFillRate(0);
      return;
    }
    setFillRate(Math.max(1, Math.min(25, n)));
  };
  return <Columns cols={2}>
            <Card title="Workload" icon="calculator">
                <div style={rowStyle}>
                    <label style={labelStyle}>Tasks per hour</label>
                    <input type="number" min="0" style={{
    ...inputStyle,
    ...numberInputStyle
  }} value={tasksPerHour} onChange={e => {
    hasInteracted.current = true;
    setTasksPerHour(parseFloat(e.target.value));
  }} />
                </div>
                <div style={rowStyle}>
                    <label style={labelStyle}>Avg session duration (min)</label>
                    <input type="number" min="0" step="0.5" style={{
    ...inputStyle,
    ...numberInputStyle
  }} value={sessionDurationMinutes} onChange={e => {
    hasInteracted.current = true;
    setSessionDurationMinutes(parseFloat(e.target.value));
  }} />
                </div>
                <div style={rowStyle}>
                    <label style={labelStyle}>Traffic pattern</label>
                    <button class="btn btn-primary dark:text-white" style={btnStyle(burstMode === 'steady')} onClick={() => {
    hasInteracted.current = true;
    setBurstMode('steady');
  }}>Steady</button>
                    <button class="btn btn-primary dark:text-white" style={btnStyle(burstMode === 'bursty')} onClick={() => {
    hasInteracted.current = true;
    setBurstMode('bursty');
  }}>Bursty</button>
                </div>
                <div style={rowStyle}>
                    <button style={disclosureStyle} onClick={() => setShowAdvanced(!showAdvanced)}>
                        {showAdvanced ? '▾' : '▸'} Advanced
                    </button>
                </div>
                {showAdvanced && <div style={rowStyle}>
                        <label style={labelStyle}>Fill rate (% / min, max 25)</label>
                        <input type="number" min="1" max="25" style={{
    ...inputStyle,
    ...numberInputStyle
  }} value={fillRate} onChange={e => setRate(e.target.value)} />
                    </div>}
                <div style={rowStyle}>
                    <span style={{
    width: '100%',
    fontSize: '0.8rem',
    fontStyle: 'italic'
  }}>
                        Assumes <code>reuse: false</code> on release. Bursty mode applies a 2× multiplier to handle peaks above the hourly average.
                    </span>
                </div>
            </Card>
            <Card title="Recommended pool size" icon="layer-group">
                <div style={rowStyle}>
                    <span style={labelStyle}>Concurrency floor:</span>
                    <span style={flashStyle}>{concurrencyFloor}</span>
                </div>
                <div style={rowStyle}>
                    <span style={labelStyle}>Refill floor:</span>
                    <span style={flashStyle}>{refillFloor}</span>
                </div>
                <div style={rowStyle}>
                    <span style={labelStyle}>Pool size:</span>
                    <span style={{
    ...flashStyle,
    fontWeight: 600
  }}>{poolSize}</span>
                </div>
                <div style={rowStyle}>
                    <span style={{
    width: '100%',
    fontSize: '0.8rem',
    fontStyle: 'italic'
  }}>
                        Binding constraint: <strong>{bindingConstraint}</strong>.
                        {bindingConstraint === 'refill' ? ' Shorter sessions or higher throughput push refill above concurrency — the 25% fill ceiling sets the floor.' : ' Longer-held browsers dominate — pool size scales with throughput × duration.'}
                    </span>
                </div>
            </Card>
        </Columns>;
};

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, allowing you to minimize browser start-up latency and scale your workloads in production.

## 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.

## 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](/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, all idle browsers are discarded and rebuilt 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, updating a pool discards all idle browsers and rebuilds them with the new configuration. Set `discard_all_idle: false` to keep existing idle browsers and only apply the new configuration to newly created browsers.

## 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).

## Pool sizing calculator

Use the calculator below to estimate a pool size for your workload. It assumes `reuse: false` on release, so every acquisition ends in destruction and triggers a refill.

<PoolSizingCalculator />

### How the calculation works

The calculator converts tasks per hour into a peak acquisition rate `λ` per minute (`tasks_per_hour / 60`, multiplied by 2× for bursty traffic) and then applies two constraints. The recommended size is the larger of the two.

**Concurrency floor.** With acquisition rate `λ` and an average acquired duration `d` (minutes), the number of browsers held simultaneously trends toward `λ × d`. We multiply by a 1.25× safety factor to keep \~10–20% of the pool available during normal load (see the [pool sizing guidance in the FAQ](/browsers/pools/faq#how-do-i-know-if-my-pool-is-too-small-or-too-large)).

```
concurrency_floor = ceil(λ × d × 1.25)
```

**Refill floor.** The [`fill_rate_per_minute`](https://kernel.sh/docs/api-reference/browser-pools/create-a-browser-pool#body-fill-rate-per-minute) is a percentage of pool size and is capped at 25. With `reuse: false`, browsers are destroyed at the acquisition rate, so the refill rate must keep up:

```
refill_floor = ceil(100 × λ / fill_rate)
```

**Recommended pool size.**

```
N = max(concurrency_floor, refill_floor)
```

The two constraints meet when average session duration ≈ `80 / fill_rate` minutes — about 3.2 minutes at the default 25% ceiling. Below that, refill sets the floor; above it, concurrency does.
