Guide
How to run several browser profiles at once with Playwright.
On this page
Playwright’s own browser contexts share one browser process, one fingerprint and the same hardware values. Donut Browser profiles are separate browsers, each with its own device, cookies and proxy. Here you launch several profiles with one call to the local API, attach Playwright to each one, run a task in all of them at the same time, and stop them. You need Pro, Team or Enterprise, Node.js or Python with Playwright, and the local API turned on.
1. Turn on the API and export the token
Open Integrations (Cmd/Ctrl+I) → Local API and turn on Enable local API server. Copy the Authentication token and export it as DONUT_API_KEY. The API listens on 127.0.0.1:10108, and only programs on your computer can reach it. See Authentication.
2. Pick the profiles
GET /v1/profiles returns every profile. It has no filter parameters, so select the profiles in your script, for example by tag:
curl -sS http://127.0.0.1:10108/v1/profiles \
-H "Authorization: Bearer $DONUT_API_KEY" |
jq -r '.profiles[] | select(.tags | index("shop")) | select(.is_running | not) | .id'Leave out profiles that already run. A batch launch does not take over a running browser; it reports PROFILE_RUNNING for it. POST /v1/profiles/{id}/run on one profile does attach to a running browser.
3. Launch the batch
POST /v1/profiles/batch/run starts the profiles and returns one debugging port per profile.
| Field | Default | Meaning |
|---|---|---|
profile_ids | required | The profiles to launch |
url | none | A page to open in each profile (http, https or about:blank) |
headless | false | Launch without windows |
concurrency | 3 | How many profiles may be starting at the same time, 1 to 20. It does not limit how many run. |
delay_ms | 800 | Pause between the starts of two launches, 0 to 60000 |
Values outside the ranges are moved to the nearest limit. The defaults come from Settings → Bulk launches: Pause between launches and Profiles starting at once.
curl -sS -X POST http://127.0.0.1:10108/v1/profiles/batch/run \
-H "Authorization: Bearer $DONUT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"profile_ids": ["<id 1>", "<id 2>", "<id 3>"], "concurrency": 3, "delay_ms": 800}'
# {"results":[{"profile_id":"<id 1>","ok":true,"remote_debugging_port":53127,"error":null}, …]}The results come in the order of profile_ids. Each browser binds its DevTools port to 127.0.0.1.
4. Attach Playwright and run the task
Connect to each port with connectOverCDP. The profile’s first context holds its cookies, so use it instead of a new context. The scripts below launch the batch, run one task in every profile at once, and stop the batch even when a task fails.
// npm install playwright
import { chromium } from "playwright";
const API = "http://127.0.0.1:10108/v1";
const headers = {
Authorization: `Bearer ${process.env.DONUT_API_KEY}`,
"Content-Type": "application/json",
};
const ids = ["<id 1>", "<id 2>", "<id 3>"];
const { results } = await fetch(`${API}/profiles/batch/run`, {
method: "POST",
headers,
body: JSON.stringify({ profile_ids: ids, concurrency: 3 }),
}).then((r) => r.json());
try {
const launched = results.filter((r) => r.ok);
for (const r of results.filter((r) => !r.ok)) console.error(r.profile_id, r.error);
await Promise.all(
launched.map(async ({ profile_id, remote_debugging_port }) => {
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${remote_debugging_port}`);
const context = browser.contexts()[0];
const page = context.pages()[0] ?? (await context.newPage());
await page.goto("https://example.com");
console.log(profile_id, await page.title());
await browser.close(); // disconnects; the browser keeps running
}),
);
} finally {
await fetch(`${API}/profiles/batch/stop`, {
method: "POST",
headers,
body: JSON.stringify({ profile_ids: ids }),
});
}# pip install playwright requests
import asyncio, os, requests
from playwright.async_api import async_playwright
API = "http://127.0.0.1:10108/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DONUT_API_KEY']}"}
IDS = ["<id 1>", "<id 2>", "<id 3>"]
async def task(p, profile_id, port):
browser = await p.chromium.connect_over_cdp(f"http://127.0.0.1:{port}")
context = browser.contexts[0]
page = context.pages[0] if context.pages else await context.new_page()
await page.goto("https://example.com")
print(profile_id, await page.title())
await browser.close() # disconnects; the browser keeps running
async def main():
results = requests.post(f"{API}/profiles/batch/run", headers=HEADERS,
json={"profile_ids": IDS, "concurrency": 3},
timeout=600).json()["results"]
try:
async with async_playwright() as p:
await asyncio.gather(*(
task(p, r["profile_id"], r["remote_debugging_port"])
for r in results if r["ok"]
))
for r in results:
if not r["ok"]:
print(r["profile_id"], r["error"])
finally:
requests.post(f"{API}/profiles/batch/stop", headers=HEADERS,
json={"profile_ids": IDS}, timeout=120)
asyncio.run(main())browser.close() on a browser you connected to only disconnects Playwright. The profile keeps running until you stop it.
5. Stop the profiles
POST /v1/profiles/batch/stop with {"profile_ids": [...]} stops the profiles one after another and returns one result per profile, with ok and error. POST /v1/profiles/{id}/kill stops one profile. In the app, select the profiles and click Stop selected.
To run the same launch from the app, select the profiles and click Run selected. Both buttons need Pro, Team or Enterprise.
Limits
- The launch routes need Pro, Team or Enterprise. On other plans they answer
402. - The Wayfern engine limits how many browsers your plan may have open at once. Past it, a launch fails with “Too many browsers are open for your plan. Close a browser and try again.” (
WAYFERN_INSTANCE_LIMIT_REACHED). - Wayfern accepts up to 100 automation (CDP) commands a minute. A command over the limit fails with “Automation rate limit exceeded (N requests/minute). Retry shortly.” Playwright sends several CDP commands for one action, so many profiles that click fast reach the limit sooner. Space out the steps, or run fewer profiles at once.
- When your plan sets an hourly limit on launch requests, the routes answer
429with aRetry-Afterheader and the bodyautomation request rate limit exceeded. A batch call counts as one request.
Troubleshooting
| Result or error | What to do |
|---|---|
PROFILE_RUNNING in a result | The profile was already open. Stop it first, or use POST /v1/profiles/{id}/run, which attaches. |
| “profile is locked by another team member” | A teammate has the profile open. See Team. |
| “cross-OS profiles cannot be launched” | The profile was made for another operating system and cannot run on this computer. |
| “launch failed: …” | Read the rest of the message. A proxy that does not answer or the browser limit of your plan are common causes. |
connectOverCDP times out | The port is from an old launch, or the profile stopped. Launch again and use the new port. |
402 | The route needs Pro, Team or Enterprise. |
Related
- Wayfern and Playwright: human-like mouse, typing and scroll for the same connection.
- Remote browsers: run profiles in Donut’s fleet instead of on your computer.
- How to create many browser profiles, each with its own proxy
- How to connect Selenium to a Donut Browser profile
Launching and driving profiles from code is part of Pro.
Last updated .