All but React Native are in beta.
All but React Native are in beta.

The Chrome Farm runs many Chromes on one computer. Each one runs your web app on its own, like a lone Chrome. Your tests or your AI agent can drive every one at the same time.
It's the web version of the Sim Farm. It's free with a Buoy account.
Plain Playwright tests already run side by side. Playwright's own workers use the same slim Chrome. On our test Mac, 64 tests took 18 s with 16 Playwright workers and 16 s with 16 farm Chromes. So for an ordinary test suite, the farm is about even. Use it when you want the things above.
Get the farm's Chrome once. It's about 100 MB.
npx -y @buoy-gg/mcp@latest farm chrome setup
npx -y @buoy-gg/mcp@latest farm chrome setup
Start 8 Chromes on your app:
npx -y @buoy-gg/mcp@latest farm chrome up 8 --url http://localhost:3000
npx -y @buoy-gg/mcp@latest farm chrome up 8 --url http://localhost:3000
Each one prints its debugging address. Playwright can use it:
const browser = await chromium.connectOverCDP(process.env.BUOY_CHROME_CDP);
const page = browser.contexts()[0].pages()[0];
const browser = await chromium.connectOverCDP(process.env.BUOY_CHROME_CDP);
const page = browser.contexts()[0].pages()[0];
Close them when you're done:
npx -y @buoy-gg/mcp@latest farm chrome down --all
npx -y @buoy-gg/mcp@latest farm chrome down --all
farm chrome run starts the Chromes, runs your command once in each, then closes them all.
npx -y @buoy-gg/mcp@latest farm chrome run 8 --url http://localhost:3000 -- node my-test.mjs
npx -y @buoy-gg/mcp@latest farm chrome run 8 --url http://localhost:3000 -- node my-test.mjs
Each run of your command gets these:
| Name | What it is |
|---|---|
BUOY_CHROME_CDP | The browser's address, for Playwright's connectOverCDP |
BUOY_CHROME_PAGE_WS | The page's address, for Puppeteer or raw CDP |
BUOY_CHROME_INDEX | Which Chrome this is, from 0 |
BUOY_CHROME_URL | Your app's address |
Got a list of jobs? Put one per line in a file and pass --jobs. The Chromes take jobs in turn. Between jobs, the farm clears what the last job left: storage, cookies, cache and extra tabs. Then it loads the app again in the same Chrome. That's about 4 times faster than starting a new Chrome. The job's line goes last on your command. It's also in BUOY_JOB. If a Chrome stops working, its jobs go to the others.
npx -y @buoy-gg/mcp@latest farm chrome run 12 --url http://localhost:3000 --jobs jobs.txt -- node my-test.mjs
npx -y @buoy-gg/mcp@latest farm chrome run 12 --url http://localhost:3000 --jobs jobs.txt -- node my-test.mjs
At the end it prints how many passed and failed. A failed job shows the end of its output.
On our test Mac, 64 tests (5 burger orders each) took 130 s on one Chrome and 23 s on 16 Chromes. Each test started its own Node and Playwright, which costs about half a second of CPU. A test runner that keeps one process per worker does better.
Start the Chromes, then give each Playwright worker its own one:
npx -y @buoy-gg/mcp@latest farm chrome up 16 --url http://localhost:3000
npx -y @buoy-gg/mcp@latest farm chrome list --json # each Chrome's browserWs
npx -y @buoy-gg/mcp@latest farm chrome up 16 --url http://localhost:3000
npx -y @buoy-gg/mcp@latest farm chrome list --json # each Chrome's browserWs
// farm.fixture.mjs: worker i drives farm Chrome i, and the app resets between tests.
import { test as base, chromium } from "@playwright/test";
const endpoints = process.env.BUOY_CHROME_ENDPOINTS.split(","); // the browserWs list, comma-separated
export const test = base.extend({
farmBrowser: [async ({}, use, workerInfo) => {
const browser = await chromium.connectOverCDP(endpoints[workerInfo.parallelIndex % endpoints.length]);
await use(browser);
await browser.close(); // disconnects; the Chrome stays
}, { scope: "worker" }],
page: async ({ farmBrowser }, use) => {
const page = farmBrowser.contexts()[0].pages()[0];
const cdp = await page.context().newCDPSession(page);
const url = new URL(page.url());
await cdp.send("Storage.clearDataForOrigin", { origin: url.origin, storageTypes: "all" });
await page.goto(url.href);
await use(page);
},
});
// farm.fixture.mjs: worker i drives farm Chrome i, and the app resets between tests.
import { test as base, chromium } from "@playwright/test";
const endpoints = process.env.BUOY_CHROME_ENDPOINTS.split(","); // the browserWs list, comma-separated
export const test = base.extend({
farmBrowser: [async ({}, use, workerInfo) => {
const browser = await chromium.connectOverCDP(endpoints[workerInfo.parallelIndex % endpoints.length]);
await use(browser);
await browser.close(); // disconnects; the Chrome stays
}, { scope: "worker" }],
page: async ({ farmBrowser }, use) => {
const page = farmBrowser.contexts()[0].pages()[0];
const cdp = await page.context().newCDPSession(page);
const url = new URL(page.url());
await cdp.send("Storage.clearDataForOrigin", { origin: url.origin, storageTypes: "all" });
await page.goto(url.href);
await use(page);
},
});
Then run npx playwright test --workers 16. On our test Mac, 64 tests took 112 s on one Chrome and 16 s on 16.
The farm runs chrome-headless-shell from Chrome for Testing, at one fixed version. It's Chrome's own engine without the browser window parts. On our test Mac, it used a quarter of full Chrome's memory with the app open:
| Full Chrome, window open | Farm Chrome | |
|---|---|---|
| Memory with the app open | 918 to 932 MB | 227 to 243 MB |
| Memory while the app is busy | 1,420 MB | 541 to 601 MB |
| Programs | 7 | 4 |
| Ready to use | 0.6 to 1.3 s | about 0.4 s |
On a Mac, each Chrome draws with the Mac's GPU, like a lone Chrome. Without the GPU, Chrome draws on the CPU. Then pages with lots of WebGL or canvas run far slower. On our test Mac, a WebGL page with 20,000 lights ran at 11 fps on the CPU. It ran at 60 fps on the GPU. Our test app used the same memory either way. To draw on the CPU anyway, add --no-gpu. Don't compare speed that way.
Every Chrome gets its own profile, so storage and cookies never mix. A fixed version means no surprise updates and no Chrome experiments that change results between runs.
The farm can hold far more Chromes open than your computer can keep busy. Memory is rarely the limit. The limit is how many of them work hard at the same moment.
We tested on an M5 Max: 18 cores (6 "Super" and 12 "Performance") and 128 GB. Every Chrome clicked through our test app nonstop, with no pauses:
| Chromes busy at once | Each step, vs one alone | Smooth frames | Work done, vs one alone |
|---|---|---|---|
| 1 | same | yes | 1x |
| 6 | 13% slower | yes | 5.9x |
| 12 | 27% slower | yes | 11.3x |
| 16 | 34% slower | yes | 14.8x |
| 24 | 2.1 times slower | yes | 17x |
Up to 16, every test passed and every page drew smooth frames with no input lag. Steps got slower for one reason. A Super core runs the app's code about 1.6 times faster than a Performance core. One Chrome alone always gets a Super core. A crowd shares 6 of them.
Real tests pause between steps to wait and check things. So we ran it again with a 300 ms pause after each step:
| Chromes at once | Each step, vs one alone | All passed, smooth frames | Work done, vs one alone |
|---|---|---|---|
| 8 | same | yes | 8x |
| 16 | same | yes | 16x |
| 24 | 4% slower | yes | 24x |
| 32 | 49% slower | yes | 32x |
| 48 | 2.9 times slower | yes | 37x |
With pauses, 24 ran at a lone Chrome's speed and 32 still did 32 times the work. farm chrome doctor checks your computer and suggests numbers. If your tests have tight time limits, raise them when you run many at once.
We also tried full Chrome windows, one per test, on the same Mac. Full Chrome dropped frames at 12. It kept a lone Chrome's speed with only 1 busy. The farm stayed smooth to 16 and kept lone speed up to 6.
We opened 100 Chromes on the test app. All 100 were ready in 55 seconds, starting 8 at a time. Together they used 31.5 GB. Closing all of them took 2.6 seconds.
An open app can still do work when no test touches it. Ours has a countdown and Buoy inside, so 100 idle copies kept 13 cores busy. Pause the ones you aren't using:
npx -y @buoy-gg/mcp@latest farm chrome pause --all
npx -y @buoy-gg/mcp@latest farm chrome resume <id>
npx -y @buoy-gg/mcp@latest farm chrome pause --all
npx -y @buoy-gg/mcp@latest farm chrome resume <id>
A paused Chrome keeps its page and its memory but uses no CPU. It wakes right away.
Frame times and input delay match a lone Chrome only up to a point. That point is the number of top cores you have. On the M5 Max that's 6.
When more are busy, take a quiet window. It pauses every other Chrome for a few seconds, so one gets the computer to itself:
npx -y @buoy-gg/mcp@latest farm chrome quiet <id> --seconds 10
npx -y @buoy-gg/mcp@latest farm chrome quiet <id> --seconds 10
Paused Chromes run no code, timers or frames. They wake up by themselves. In our test, one Chrome took 124 ms per step with 11 busy neighbors. It took 54 ms alone and 46 ms in a quiet window. Then farm chrome list shows how much of its work ran on the fastest cores.
With --boost, the Chrome a test is driving right now gets one of the computer's fastest cores. The others sit a little lower until their next step. Only scheduling priority changes, so timers and frames behave as before.
npx -y @buoy-gg/mcp@latest farm chrome up 32 --url http://localhost:3000 --boost
npx -y @buoy-gg/mcp@latest farm chrome up 32 --url http://localhost:3000 --boost
It helps when tests pause between steps, like most real tests. On our test Mac, with 32 to 48 Chromes pausing between steps, steps took 6% to 24% less time with boost. It helped more when the Mac was busier. There's a cost: with 48 Chromes, the slowest Chrome's input delay went from about 55 ms to about 80 ms. Boost doesn't help tests that click nonstop, since then every Chrome is mid-step. It needs a Mac with Xcode's command line tools. The farm builds a tiny helper the first time.
One Chrome can't take down the rest.
--cap-mb.Each Chrome is a full copy of your app. If your app calls a server when it starts, 50 Chromes call it 50 times. Point your app at a test server, or block calls you don't want repeated.
npx -y @buoy-gg/mcp@latest farm chrome setup # get the farm's Chrome
npx -y @buoy-gg/mcp@latest farm chrome up 8 --url <app> # start 8
npx -y @buoy-gg/mcp@latest farm chrome list # memory, CPU and addresses
npx -y @buoy-gg/mcp@latest farm chrome reset --all # clear storage, reload the app
npx -y @buoy-gg/mcp@latest farm chrome pause --all # keep pages, use no CPU
npx -y @buoy-gg/mcp@latest farm chrome resume --all # wake them
npx -y @buoy-gg/mcp@latest farm chrome shot <id> a.png # save a screenshot
npx -y @buoy-gg/mcp@latest farm chrome down --all # close them
npx -y @buoy-gg/mcp@latest farm chrome doctor # cores, memory, suggested counts
npx -y @buoy-gg/mcp@latest farm chrome stop # close all and stop the farm
npx -y @buoy-gg/mcp@latest farm chrome setup # get the farm's Chrome
npx -y @buoy-gg/mcp@latest farm chrome up 8 --url <app> # start 8
npx -y @buoy-gg/mcp@latest farm chrome list # memory, CPU and addresses
npx -y @buoy-gg/mcp@latest farm chrome reset --all # clear storage, reload the app
npx -y @buoy-gg/mcp@latest farm chrome pause --all # keep pages, use no CPU
npx -y @buoy-gg/mcp@latest farm chrome resume --all # wake them
npx -y @buoy-gg/mcp@latest farm chrome shot <id> a.png # save a screenshot
npx -y @buoy-gg/mcp@latest farm chrome down --all # close them
npx -y @buoy-gg/mcp@latest farm chrome doctor # cores, memory, suggested counts
npx -y @buoy-gg/mcp@latest farm chrome stop # close all and stop the farm
Add --desktop for a 1280 × 800 window. The default is a phone: 402 × 874 at 3x, with touch.
| Tool | What it does |
|---|---|
chrome_farm_status | Running Chromes, their memory and CPU, and suggested counts |
chrome_farm_up | Starts Chromes on your app and returns their addresses |
chrome_farm_reset | Clears storage and reloads the app |
chrome_farm_pause | Pauses or wakes Chromes you aren't using |
chrome_farm_quiet | Pauses the others for a fair speed sample |
chrome_farm_screenshot | A picture of one Chrome |
chrome_farm_down | Closes Chromes |
If your app has Buoy inside, each Chrome also shows up in list_devices as its own web device. Then every Buoy tool works on it.
An agent only touches the Chromes it started. Its quiet windows, all: true and pauses never reach another session's Chromes. From the command line, farm chrome quiet pauses every other Chrome.