diff --git a/browsers/pools/overview.mdx b/browsers/pools/overview.mdx index d3ffd1c..26b791f 100644 --- a/browsers/pools/overview.mdx +++ b/browsers/pools/overview.mdx @@ -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. + + + +### 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. diff --git a/snippets/pool-sizing-calculator.jsx b/snippets/pool-sizing-calculator.jsx new file mode 100644 index 0000000..bfd1c0d --- /dev/null +++ b/snippets/pool-sizing-calculator.jsx @@ -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: '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 ( + + +
+ + { hasInteracted.current = true; setTasksPerHour(parseFloat(e.target.value)); }} /> +
+
+ + { hasInteracted.current = true; setSessionDurationMinutes(parseFloat(e.target.value)); }} /> +
+
+ + + +
+
+ +
+ {showAdvanced && ( +
+ + setRate(e.target.value)} /> +
+ )} +
+ + Assumes reuse: false on release. Bursty mode applies a 2× multiplier to handle peaks above the hourly average. + +
+
+ +
+ Concurrency floor: + {concurrencyFloor} +
+
+ Refill floor: + {refillFloor} +
+
+ Pool size: + {poolSize} +
+
+ + Binding constraint: {bindingConstraint}. + {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.'} + +
+
+
+ ); +};