Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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