docs

Chrome Farm

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.

What it's for

  • AI agents. Your agent starts the Chromes it needs. It keeps them open while it works. With Buoy in your app, each Chrome is its own device for Buoy tools.
  • Tests and speed checks that need care. Each Chrome has a memory cap. You can pause it. It can also get the computer to itself for a fair speed check.
  • Knowing how many. We measured how many Chromes a Mac like ours keeps smooth. The farm tells you.

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.

Start

Get the farm's Chrome once. It's about 100 MB.

bash
npx -y @buoy-gg/mcp@latest farm chrome setup
npx -y @buoy-gg/mcp@latest farm chrome setup

Start 8 Chromes on your app:

bash
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:

js
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:

bash
npx -y @buoy-gg/mcp@latest farm chrome down --all
npx -y @buoy-gg/mcp@latest farm chrome down --all

Run your tests side by side

farm chrome run starts the Chromes, runs your command once in each, then closes them all.

bash
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:

NameWhat it is
BUOY_CHROME_CDPThe browser's address, for Playwright's connectOverCDP
BUOY_CHROME_PAGE_WSThe page's address, for Puppeteer or raw CDP
BUOY_CHROME_INDEXWhich Chrome this is, from 0
BUOY_CHROME_URLYour 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.

bash
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.

Playwright Test

Start the Chromes, then give each Playwright worker its own one:

bash
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
js
// 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.

Why the farm's Chrome is slim

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 openFarm Chrome
Memory with the app open918 to 932 MB227 to 243 MB
Memory while the app is busy1,420 MB541 to 601 MB
Programs74
Ready to use0.6 to 1.3 sabout 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.

How many to run

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 onceEach step, vs one aloneSmooth framesWork done, vs one alone
1sameyes1x
613% sloweryes5.9x
1227% sloweryes11.3x
1634% sloweryes14.8x
242.1 times sloweryes17x

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 onceEach step, vs one aloneAll passed, smooth framesWork done, vs one alone
8sameyes8x
16sameyes16x
244% sloweryes24x
3249% sloweryes32x
482.9 times sloweryes37x

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.

100 at once

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:

bash
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.

Fair speed numbers

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:

bash
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.

Boost

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.

bash
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.

Caps

One Chrome can't take down the rest.

  • Memory cap. Each Chrome has a cap, 2 GB by default. A Chrome that goes over it is closed. Set it with --cap-mb.
  • Hangs. The farm checks every Chrome every 15 seconds and notes the ones that stop answering.
  • Own process. Each Chrome runs alone. A crash only ends that one.

Your app's own calls

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.

Farm commands

bash
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.

From an agent

ToolWhat it does
chrome_farm_statusRunning Chromes, their memory and CPU, and suggested counts
chrome_farm_upStarts Chromes on your app and returns their addresses
chrome_farm_resetClears storage and reloads the app
chrome_farm_pausePauses or wakes Chromes you aren't using
chrome_farm_quietPauses the others for a fair speed sample
chrome_farm_screenshotA picture of one Chrome
chrome_farm_downCloses 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.

Limits

  • Mac first. Linux and Windows are not tested yet. There the farm doesn't pick the GPU yet. Quiet windows and pausing use Unix signals, so Windows needs more work.
  • Chromes run without a window. You see them through screenshots.
  • Speed numbers come from your computer. Compare ideas inside one run.