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/6] 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/6] 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 | From cea97e80db2d0aee6fff9c9ffd809d5b7c7378f1 Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Thu, 4 Jun 2026 20:37:17 +0000 Subject: [PATCH 3/6] v2: customer-friendly inputs for pool sizing calculator - swap acquisitions/min for tasks/hour (more natural unit) - add steady vs bursty toggle (2x multiplier on peak rate) - move fill rate behind an "Advanced" disclosure, default 25 - update prose to describe the new inputs Co-Authored-By: Claude Opus 4.7 --- browsers/pools/overview.mdx | 4 +- snippets/pool-sizing-calculator.jsx | 60 +++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/browsers/pools/overview.mdx b/browsers/pools/overview.mdx index 6491771..267da6a 100644 --- a/browsers/pools/overview.mdx +++ b/browsers/pools/overview.mdx @@ -472,9 +472,9 @@ Use the calculator below to estimate a pool size for your workload. It assumes ` ### How the calculation works -Two constraints have to be satisfied at the same time, so the recommended size is the larger of the two. +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 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.** 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) diff --git a/snippets/pool-sizing-calculator.jsx b/snippets/pool-sizing-calculator.jsx index a5d4c83..ed2a993 100644 --- a/snippets/pool-sizing-calculator.jsx +++ b/snippets/pool-sizing-calculator.jsx @@ -2,11 +2,13 @@ const { useState, useEffect, useRef } = React; const { Card, Columns } = MintlifyComponents; export const PoolSizingCalculator = () => { - const defaults = { acquisitionRate: 10, sessionDurationMinutes: 5, fillRate: 25 }; + const defaults = { tasksPerHour: 600, sessionDurationMinutes: 5, burstMode: 'steady', fillRate: 25 }; - const [acquisitionRate, setAcquisitionRate] = useState(defaults.acquisitionRate); + 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); @@ -14,18 +16,21 @@ export const PoolSizingCalculator = () => { useEffect(() => { if (!hasInteracted.current) return; var url = new URL(window.location); - url.searchParams.set('acquisitionRate', acquisitionRate); + 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); - }, [acquisitionRate, sessionDurationMinutes, fillRate]); + }, [tasksPerHour, sessionDurationMinutes, burstMode, fillRate]); const safety = 1.25; - const lambda = Number.isFinite(acquisitionRate) && acquisitionRate > 0 ? acquisitionRate : 0; + 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); @@ -46,6 +51,17 @@ export const PoolSizingCalculator = () => { 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; @@ -58,23 +74,37 @@ export const PoolSizingCalculator = () => {
- - { hasInteracted.current = true; setAcquisitionRate(parseFloat(e.target.value)); }} /> + + { hasInteracted.current = true; setTasksPerHour(parseFloat(e.target.value)); }} />
- + { hasInteracted.current = true; setSessionDurationMinutes(parseFloat(e.target.value)); }} />
- - setRate(e.target.value)} /> + + + +
+
+
+ {showAdvanced && ( +
+ + setRate(e.target.value)} /> +
+ )}
- Assumes reuse: false on release (every acquisition triggers a refill). Safety factor 1.25× covers the recommended 10–20% headroom. + Assumes reuse: false on release. Bursty mode applies a 2× multiplier to handle peaks above the hourly average.
@@ -95,8 +125,8 @@ export const PoolSizingCalculator = () => { 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.'} + ? ' 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.'} From fc726c0bca50f9425389dae87bf13e3ae5e055a7 Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Fri, 5 Jun 2026 04:18:19 +0000 Subject: [PATCH 4/6] clarify crossover threshold depends on fill rate Co-Authored-By: Claude Opus 4.7 --- browsers/pools/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browsers/pools/overview.mdx b/browsers/pools/overview.mdx index 267da6a..26b791f 100644 --- a/browsers/pools/overview.mdx +++ b/browsers/pools/overview.mdx @@ -492,4 +492,4 @@ refill_floor = ceil(100 × λ / fill_rate) 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. +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. From 93b452c2360a6c2f7d60a5b2bc6ce3d6e18cd754 Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:26:06 +0000 Subject: [PATCH 5/6] fix pool sizing calculator row overflow narrow labels to 10rem and allow rows to wrap so the traffic pattern buttons don't clip off the right edge of the workload card. --- snippets/pool-sizing-calculator.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snippets/pool-sizing-calculator.jsx b/snippets/pool-sizing-calculator.jsx index ed2a993..7afb778 100644 --- a/snippets/pool-sizing-calculator.jsx +++ b/snippets/pool-sizing-calculator.jsx @@ -46,8 +46,8 @@ export const PoolSizingCalculator = () => { 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 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' }; From b66f1d5a64aa1b8a3afd46400089c481e63a7edc Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:37:47 +0000 Subject: [PATCH 6/6] =?UTF-8?q?drop=202=C3=97=20from=20bursty=20button=20l?= =?UTF-8?q?abel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit frees up enough room for steady and bursty to sit on the same row as the traffic pattern label. the 2× multiplier is already explained in the helper text below. --- snippets/pool-sizing-calculator.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snippets/pool-sizing-calculator.jsx b/snippets/pool-sizing-calculator.jsx index 7afb778..bfd1c0d 100644 --- a/snippets/pool-sizing-calculator.jsx +++ b/snippets/pool-sizing-calculator.jsx @@ -88,7 +88,7 @@ export const PoolSizingCalculator = () => { + onClick={() => { hasInteracted.current = true; setBurstMode('bursty'); }}>Bursty