Skip to content
32 changes: 32 additions & 0 deletions browsers/pools/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -461,3 +461,35 @@ func main() {
## 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

import { PoolSizingCalculator } from '/snippets/pool-sizing-calculator.jsx';

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

Two constraints have to be satisfied at the same time, so the recommended size is the larger of the two.

**Concurrency floor.** With a peak acquisition rate `λ` (per minute) 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)
```

At the 25% fill ceiling, the two constraints meet at `d ≈ 3.2` minutes. Below that, the refill ceiling sets the floor; above it, concurrency does.
105 changes: 105 additions & 0 deletions snippets/pool-sizing-calculator.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
const { useState, useEffect, useRef } = React;
const { Card, Columns } = MintlifyComponents;

export const PoolSizingCalculator = () => {
const defaults = { acquisitionRate: 10, sessionDurationMinutes: 5, fillRate: 25 };

const [acquisitionRate, setAcquisitionRate] = useState(defaults.acquisitionRate);
const [sessionDurationMinutes, setSessionDurationMinutes] = useState(defaults.sessionDurationMinutes);
const [fillRate, setFillRate] = useState(defaults.fillRate);
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('acquisitionRate', acquisitionRate);
url.searchParams.set('sessionDuration', sessionDurationMinutes);
url.searchParams.set('fillRate', fillRate);
url.hash = 'pool-sizing-calculator';
window.history.replaceState(null, '', url);
}, [acquisitionRate, sessionDurationMinutes, fillRate]);

const safety = 1.25;
const lambda = Number.isFinite(acquisitionRate) && acquisitionRate > 0 ? acquisitionRate : 0;
const duration = Number.isFinite(sessionDurationMinutes) && sessionDurationMinutes > 0 ? sessionDurationMinutes : 0;
const rate = Number.isFinite(fillRate) && fillRate > 0 ? Math.min(fillRate, 25) : 1;

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: '12rem', flexShrink: 0, maxWidth: '12rem' };
const rowStyle = { display: 'flex', alignItems: 'center', gap: '0.5rem', minHeight: '2.25rem' };
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 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}>Acquisitions / minute</label>
<input type="number" min="0" style={{...inputStyle, ...numberInputStyle}} value={acquisitionRate}
onChange={(e) => { hasInteracted.current = true; setAcquisitionRate(parseFloat(e.target.value)); }} />
</div>
<div style={rowStyle}>
<label style={labelStyle}>Avg acquired 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}>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 (every acquisition triggers a refill). Safety factor 1.25× covers the recommended 10–20% headroom.
</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 acquisition rates push refill above concurrency — the 25% fill ceiling sets the floor.'
: ' Longer-held browsers dominate — pool size scales with acquisitions × duration.'}
</span>
</div>
</Card>
</Columns>
);
};
Loading