> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenrows.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> The Zenrows API (Fetch, Extract via extract=auto, and Batch) is described in OpenAPI 3.1 at https://www.zenrows.com/openapi.json and listed in the RFC 9727 API catalog at https://www.zenrows.com/.well-known/api-catalog. Use the spec for exact parameter names and types.

# Run browser sessions in parallel

> Run several Zenrows Browser sessions side by side, each with its own residential IP, to finish browser jobs faster.

Scale a browser job across many pages with Zenrows Browser, the browser for agents, by opening several sessions at once. Each connection is its own cloud browser with its own residential IP, so the sessions work side by side.

**Uses:** [Browser](/browser-sessions/introduction) with Playwright, `asyncio.Semaphore` (Python) and a worker pool (Node.js)

## Ask your AI agent

With the [Zenrows MCP server](/mcp/overview) connected to Claude, Cursor or any MCP client, paste this prompt. Your agent picks the right Zenrows tools and settings on its own.

```text theme={"dark"}
Use the Zenrows browser to open these product pages side by side, one session each, and give me
a table with each product's name and price. Close every browser session when you finish.
https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/
https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/
https://www.scrapingcourse.com/ecommerce/product/aeon-capri/
```

## Scrape pages in parallel

Each task connects, scrapes one page and closes its browser. `MAX_SESSIONS` sets how many run together, matched to your plan's concurrency.

<CodeGroup>
  ```python Python (Playwright) theme={"dark"}
  import asyncio
  from playwright.async_api import async_playwright

  CONNECTION_URL = "wss://browser.zenrows.com?apikey=YOUR_ZENROWS_API_KEY"
  MAX_SESSIONS = 2  # browser sessions open at the same time

  URLS = [
      "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/",
      "https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/",
      "https://www.scrapingcourse.com/ecommerce/product/aeon-capri/",
  ]

  async def scrape(p, semaphore, url):
      async with semaphore:
          # Each connect_over_cdp call opens its own browser session with its own IP
          browser = await p.chromium.connect_over_cdp(CONNECTION_URL)
          try:
              page = await browser.contexts[0].new_page()
              await page.route("**/*.{png,jpg,jpeg,webp,gif,svg}", lambda route: route.abort())
              await page.goto(url, wait_until="domcontentloaded")
              return {
                  "url": url,
                  "name": await page.inner_text("h1.product_title"),
                  "price": await page.inner_text(".summary .price"),
              }
          finally:
              await browser.close()

  async def main():
      semaphore = asyncio.Semaphore(MAX_SESSIONS)
      async with async_playwright() as p:
          results = await asyncio.gather(
              *(scrape(p, semaphore, url) for url in URLS), return_exceptions=True
          )
      for result in results:
          print(result)

  asyncio.run(main())
  ```

  ```javascript Node.js (Playwright) theme={"dark"}
  import { chromium } from "playwright-core";

  const connectionURL = "wss://browser.zenrows.com?apikey=YOUR_ZENROWS_API_KEY";
  const MAX_SESSIONS = 2; // browser sessions open at the same time

  const urls = [
    "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/",
    "https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/",
    "https://www.scrapingcourse.com/ecommerce/product/aeon-capri/",
  ];

  async function scrape(url) {
    // Each connectOverCDP call opens its own browser session with its own IP
    const browser = await chromium.connectOverCDP(connectionURL);
    try {
      const page = await browser.contexts()[0].newPage();
      await page.route("**/*.{png,jpg,jpeg,webp,gif,svg}", (route) => route.abort());
      await page.goto(url, { waitUntil: "domcontentloaded" });
      return {
        url,
        name: await page.innerText("h1.product_title"),
        price: await page.innerText(".summary .price"),
      };
    } finally {
      await browser.close();
    }
  }

  // MAX_SESSIONS workers pull URLs from a shared queue
  const queue = [...urls];
  const results = [];
  await Promise.all(
    Array.from({ length: MAX_SESSIONS }, async () => {
      while (queue.length) {
        const url = queue.shift();
        try {
          results.push(await scrape(url));
        } catch (error) {
          results.push({ url, error: error.message });
        }
      }
    }),
  );
  console.log(results);
  ```
</CodeGroup>

Errors stay with their page: `return_exceptions=True` in Python and the `try`/`catch` in each Node.js worker record a failure and let the other sessions carry on. Retry the failed URLs in a second pass.

## Example output

```text theme={"dark"}
{'url': 'https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/', 'name': 'Abominable Hoodie', 'price': '$69.00'}
{'url': 'https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/', 'name': 'Adrienne Trek Jacket', 'price': '$57.00'}
{'url': 'https://www.scrapingcourse.com/ecommerce/product/aeon-capri/', 'name': 'Aeon Capri', 'price': '$48.00'}
```

## Customize it

| Change | How |
| - | - |
| More sessions at once | Raise `MAX_SESSIONS` to match your plan's concurrency, see [Pricing](/first-steps/pricing) |
| Several pages per session | Loop over a slice of URLs inside one `scrape` call and reuse the page, which saves session start-up time |
| A country per session | Build the URL per task, for example `&proxy_country=us` for one list and `&proxy_country=de` for another, see [Country targeting](/browser-sessions/features/country) |
| Isolated logins in one session | `await browser.new_context(storage_state=...)` per account, see [Log in once and reuse the session](/academy/browser/login-and-reuse-sessions) |
| Thousands of static pages | Send them as one job with Batch, see [Scrape a list of URLs](/academy/scrape-from-a-list-of-urls) |
| Parallel requests without a browser | Use Fetch with a concurrency limit, see [Send concurrent requests](/academy/fetch/concurrent-requests) |

## Troubleshooting

* **Some connections are refused while others run**: more sessions are open than your plan's concurrency. Lower `MAX_SESSIONS`, and make sure every task closes its browser in `finally`.
* **The job slows down as it runs**: sessions from earlier runs are still open. Close every browser explicitly, and set a short `session_ttl` such as `2m` so forgotten sessions end on their own.
* **Results come back in a different order**: workers finish at different times. Key results by URL, as the examples do.
* **One page keeps failing**: the page needs more time or a different country. Retry it alone with `wait_until="load"` or a `proxy_country`.

## Related recipes

<CardGroup cols={2}>
  <Card title="Connect Playwright or Puppeteer to Zenrows Browser" icon="plug" href="/academy/browser/connect-playwright-and-puppeteer">
    The connection URL and its country, region and lifetime options.
  </Card>

  <Card title="Scroll and paginate to collect every item" icon="list-details" href="/academy/browser/infinite-scroll-and-pagination">
    Collect full listings, then fan out to their detail pages.
  </Card>

  <Card title="Scrape a list of URLs" icon="list" href="/academy/scrape-from-a-list-of-urls">
    Send thousands of URLs as one Batch job.
  </Card>

  <Card title="Fetch first, open a Browser when a task needs interaction" icon="git-branch" href="/academy/pipelines/fetch-with-browser-fallback">
    Use Fetch first and open a browser for the pages that need it.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.