From 1629c51f0d37e1857d510b51c91fa96127828a48 Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:22:42 +0000 Subject: [PATCH 1/2] add pool sizing calculator to reserved browsers overview Adds an interactive calculator under the Browser Pools overview page that recommends a pool size from acquisition rate, average acquired duration, and fill rate. Includes the underlying formula (concurrency floor vs refill floor) and worked examples. Co-Authored-By: Claude Opus 4.7 --- browsers/pools/overview.mdx | 41 +++++++++++ snippets/pool-sizing-calculator.jsx | 105 ++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 snippets/pool-sizing-calculator.jsx diff --git a/browsers/pools/overview.mdx b/browsers/pools/overview.mdx index d3ffd1c..99e4409 100644 --- a/browsers/pools/overview.mdx +++ b/browsers/pools/overview.mdx @@ -461,3 +461,44 @@ 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 + +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. + +### Worked examples + +| Acquisitions/min | Duration (min) | Refill floor | Concurrency floor | Pool size | Binding | +| --- | --- | --- | --- | --- | --- | +| 10 | 1 | 40 | 13 | **40** | refill | +| 10 | 5 | 40 | 63 | **63** | concurrency | +| 30 | 2 | 120 | 75 | **120** | refill | +| 30 | 10 | 120 | 375 | **375** | concurrency | diff --git a/snippets/pool-sizing-calculator.jsx b/snippets/pool-sizing-calculator.jsx new file mode 100644 index 0000000..a5d4c83 --- /dev/null +++ b/snippets/pool-sizing-calculator.jsx @@ -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 ( + + +
+ + { hasInteracted.current = true; setAcquisitionRate(parseFloat(e.target.value)); }} /> +
+
+ + { hasInteracted.current = true; setSessionDurationMinutes(parseFloat(e.target.value)); }} /> +
+
+ + setRate(e.target.value)} /> +
+
+ + Assumes reuse: false on release (every acquisition triggers a refill). Safety factor 1.25× covers the recommended 10–20% headroom. + +
+
+ +
+ Concurrency floor: + {concurrencyFloor} +
+
+ Refill floor: + {refillFloor} +
+
+ Pool size: + {poolSize} +
+
+ + Binding constraint: {bindingConstraint}. + {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.'} + +
+
+
+ ); +}; From 9a4340c9273e812a08ed407caf11b9524f912847 Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:44:50 +0000 Subject: [PATCH 2/2] remove worked-examples table from pool sizing section Co-Authored-By: Claude Opus 4.7 --- browsers/pools/overview.mdx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/browsers/pools/overview.mdx b/browsers/pools/overview.mdx index 99e4409..6491771 100644 --- a/browsers/pools/overview.mdx +++ b/browsers/pools/overview.mdx @@ -493,12 +493,3 @@ 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. - -### Worked examples - -| Acquisitions/min | Duration (min) | Refill floor | Concurrency floor | Pool size | Binding | -| --- | --- | --- | --- | --- | --- | -| 10 | 1 | 40 | 13 | **40** | refill | -| 10 | 5 | 40 | 63 | **63** | concurrency | -| 30 | 2 | 120 | 75 | **120** | refill | -| 30 | 10 | 120 | 375 | **375** | concurrency |