DEV Community

Isaias Velasquez
Isaias Velasquez

Posted on

[BA-008] Running multiple browsers in parallel

When you need to scrape multiple pages or test across different scenarios, running one browser at a time is too slow. Playwright supports concurrent contexts and browsers natively.

The simplest way to run tasks in parallel is with Python threads:

from playwright.sync_api import sync_playwright
from concurrent.futures import ThreadPoolExecutor

def check_page(url: str) -> dict:
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        page.goto(url, wait_until="domcontentloaded")
        result = {
            "url": url,
            "title": page.title(),
            "status": page.evaluate("document.readyState")
        }
        browser.close()
        return result

urls = [
    "https://example.com",
    "https://example.org",
    "https://example.net",
]

with ThreadPoolExecutor(max_workers=3) as pool:
    results = list(pool.map(check_page, urls))

for r in results:
    print(f"{r['url']}: {r['title']}")
Enter fullscreen mode Exit fullscreen mode

Playwright sync API is thread safe when you create a separate browser instance per thread. Each thread gets its own browser process.

For better performance you can reuse browser instances across tasks. This avoids the overhead of launching a browser for every task:

from playwright.sync_api import sync_playwright

class BrowserPool:
    def __init__(self, size=3):
        self.playwright = sync_playwright().start()
        self.browsers = [
            self.playwright.chromium.launch()
            for _ in range(size)
        ]
        self.index = 0

    def get_page(self):
        browser = self.browsers[self.index % len(self.browsers)]
        self.index += 1
        return browser.new_page()

    def close(self):
        for b in self.browsers:
            b.close()
        self.playwright.stop()

pool = BrowserPool(3)

pages = [pool.get_page() for _ in range(6)]
for page in pages:
    page.goto("https://example.com")
    print(page.title())

for page in pages:
    page.close()
pool.close()
Enter fullscreen mode Exit fullscreen mode

This keeps 3 browser processes running and distributes pages across them.

A common production pattern is to use Playwright async API with asyncio:

import asyncio
from playwright.async_api import async_playwright

async def scrape(url: str):
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto(url)
        title = await page.title()
        await browser.close()
        return title

async def main():
    tasks = [
        scrape("https://example.com"),
        scrape("https://example.org"),
        scrape("https://example.net"),
    ]
    results = await asyncio.gather(*tasks)
    for url, title in zip(urls, results):
        print(f"{url}: {title}")

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

The async version uses less memory because it runs in a single event loop instead of one thread per task.

A few things to keep in mind when running parallel browsers:

  • Each browser process uses about 200-400 MB of RAM
  • Websites rate limit requests per IP, so parallel requests from the same IP may be blocked
  • Always call browser.close() to free resources
  • For large scale work, use a queue based system instead of unbounded parallelism

Parallel browsers turn a 60 second scraping job into 10 seconds.

That's all for now.
Thanks for reading!

Top comments (0)