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

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.
135 changes: 135 additions & 0 deletions snippets/pool-sizing-calculator.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const { useState, useEffect, useRef } = React;
const { Card, Columns } = MintlifyComponents;

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: '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 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 (2×)</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>
);
};
Loading