---
title: "How to run several browser profiles at once with Playwright"
description: "Launch a batch of Donut Browser profiles with one API call, attach Playwright to each over CDP, run the same task in all of them, and stop them cleanly."
url: "https://donutbrowser.com/docs/guides/run-profiles-in-parallel/"
---

# How to run several browser profiles at once with Playwright

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](https://donutbrowser.com/docs/quickstart) 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](https://donutbrowser.com/docs/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:

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

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

```javascript
// 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 }),
  });
}
```

```python
# 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 `429` with a `Retry-After` header and the body `automation 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](https://donutbrowser.com/docs/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](https://donutbrowser.com/docs/wayfern): human-like mouse, typing and scroll for the same connection.
- [Remote browsers](https://donutbrowser.com/docs/remote-browsers): run profiles in Donut's fleet instead of on your computer.
- [How to create many browser profiles, each with its own proxy](https://donutbrowser.com/docs/guides/bulk-create-profiles)
- [How to connect Selenium to a Donut Browser profile](https://donutbrowser.com/docs/guides/selenium)

Launching and driving profiles from code is part of [Pro](https://donutbrowser.com/pricing).
