David Lewin David Lewin
Proxies & Web Scraping

Move your Playwright scrapers to hosted Chrome: connectOverCDP vs a VPS cluster

Disclosure: some links in this guide are affiliate links. If you sign up through them, I may earn a commission at no extra cost to you. How this site makes money.

Short answer

Replace chromium.launch() with chromium.connect_over_cdp('wss://production-sfo.browserless.io?token=...') and your existing Playwright code drives a Chrome running on Browserless instead of your own server. Browserless bills units of up to 30 seconds of browser time per connection, so how you connect decides the bill: opening one session per page costs at least a unit a page, while reusing each session for many pages costs only the seconds you use. For 100,000 six-second pages a month, that's about $35 with reused sessions or $200 with a session per page, against about $124 for a 4 GB VPS plus an estimated two hours of upkeep. Keep workers at or below your plan's concurrent browsers, reconnect before the session time cap, and always close the browser.

Headless Chrome is the expensive part of a Playwright scraper. Each browser takes hundreds of megabytes of RAM and a good share of a CPU core, crashes now and then, and needs updating along with Playwright. Hosted browser services run the browsers for you: your script stays the same, but instead of launching Chromium locally it connects to one in the cloud over the Chrome DevTools Protocol (CDP). This guide shows how that connection works with Browserless, how to write a worker pool that survives dropped sessions without wasting paid browser time, and when a high-RAM VPS from Vultr or DigitalOcean is still the cheaper home for your browsers.

How this guide was made

The worker pool below was tested with Playwright for Python 1.64 against a local Chromium-based browser exposed over CDP, using 300 pages and one deliberately killed browser mid-run: all 300 pages were fetched after automatic reconnects. It wasn't run against Browserless itself. Browserless plans, units and connection URLs come from its pricing page and documentation, and Droplet prices from DigitalOcean's pricing page, all checked in October 2026. Memory per browser and maintenance hours are our estimates.

1. How connectOverCDP works

Chrome can be controlled over a WebSocket using CDP, the same protocol its DevTools use. Playwright's connect_over_cdp() (connectOverCDP() in Node.js) attaches to a browser that's already running somewhere else, so your script only sends commands and receives results.

Playwright with hosted Chrome over CDP
Playwright with hosted Chrome over CDPYour Playwright script → connect_over_cdp over a WebSocket → Hosted Chrome on Browserless → Target websiteYour Playwright script: Runs on a laptop, in CI or on a small VPS. No browser installed.Your Playwright scriptRuns on a laptop, in CI or on a small VPS. No browserinstalled.connect_over_cdp over a WebSocket: wss://production-sfo.browserless.io?token=... One WebSocket per browser session.connect_over_cdp over a WebSocketwss://production-sfo.browserless.io?token=... One WebSocket perbrowser session.Hosted Chrome on Browserless: Runs the pages. Billed in units of up to 30 seconds per connection.Hosted Chrome on BrowserlessRuns the pages. Billed in units of up to 30 seconds perconnection.Target websiteTarget website
Only the commands and results cross the network. The rendering, memory and CPU are on the provider's side. Hover or tab through the boxes for details.

Moving an existing script takes one change:

# Before: a browser on your own machine
browser = await pw.chromium.launch()

# After: a browser on Browserless (regions: production-sfo, production-lon, production-ams)
browser = await pw.chromium.connect_over_cdp(
    'wss://production-sfo.browserless.io?token=YOUR_TOKEN&timeout=600000', timeout=60_000)

Three details to know:

  • Existing context. A CDP connection comes with the browser's default context already open in browser.contexts[0]. You can use it or create a new one.
  • CDP vs Playwright's own protocol. Playwright's docs note that a CDP connection has lower fidelity than chromium.connect(), Playwright's native protocol. Browserless also offers a native endpoint at /chromium/playwright, but it requires your Playwright version to match theirs. CDP works across versions, which is why it's the common choice.
  • Every call is a network round trip. A script that makes hundreds of small locator calls per page will be slower than on localhost. Pull the data you need in one page.evaluate() call, and pick the region closest to where your script runs.

2. A worker pool that reuses sessions and survives drops

#!/usr/bin/env python3
"""Run Playwright against hosted Chrome over CDP: a few long-lived sessions, reconnect on drops.

  pip install playwright           # no browser download needed, the browser is remote
  export BROWSERLESS_TOKEN=...
  python3 hosted_scrape.py urls.txt > titles.csv
"""
import asyncio
import csv
import os
import sys

from playwright.async_api import async_playwright

WS = os.environ.get('CDP_URL') or (
    'wss://production-sfo.browserless.io?token=%s&timeout=600000' % os.environ['BROWSERLESS_TOKEN'])  # session cap: 10 min
CONCURRENCY = 5          # keep at or below your plan's concurrent browsers
PAGES_PER_SESSION = 50   # reconnect before the session cap, and to start each batch with a fresh browser
MAX_TRIES = 3


async def connect(pw):
    """Connect with backoff: a 429 or refused connection usually means all your browsers are busy."""
    for attempt in range(6):
        try:
            return await pw.chromium.connect_over_cdp(WS, timeout=60_000)
        except Exception as e:
            wait = min(2 ** attempt, 30)
            print('connect failed (%s), retrying in %ds' % (str(e).splitlines()[0][:80], wait), file=sys.stderr)
            await asyncio.sleep(wait)
    raise RuntimeError('could not connect to the browser service')


async def worker(pw, queue, tries, out):
    while not queue.empty():
        browser = await connect(pw)
        try:
            context = browser.contexts[0] if browser.contexts else await browser.new_context()
            page = await context.new_page()
            page.set_default_timeout(30_000)  # one slow page fails on its own instead of holding the session
            for _ in range(PAGES_PER_SESSION):
                try:
                    url = queue.get_nowait()
                except asyncio.QueueEmpty:
                    return
                try:
                    await page.goto(url, wait_until='domcontentloaded')
                    out.writerow([url, await page.title()])
                except Exception as e:
                    tries[url] = tries.get(url, 0) + 1
                    if tries[url] < MAX_TRIES:
                        queue.put_nowait(url)  # retry later, possibly on a new session
                    else:
                        out.writerow([url, 'ERROR ' + str(e).splitlines()[0][:80]])
                    break  # the session may have dropped or hit its time cap: reconnect before the next page
        finally:
            if browser.is_connected():
                await browser.close()  # close explicitly: an open session keeps using paid browser time


async def main():
    queue = asyncio.Queue()
    for line in open(sys.argv[1]):
        if line.strip():
            queue.put_nowait(line.strip())
    out = csv.writer(sys.stdout)
    out.writerow(['url', 'title'])
    tries = {}
    async with async_playwright() as pw:
        await asyncio.gather(*(worker(pw, queue, tries, out) for _ in range(CONCURRENCY)))


if __name__ == '__main__':
    asyncio.run(main())

Replace page.title() with your own extraction. Set CDP_URL to point the same script at any CDP endpoint, including a Chrome you run yourself, which is how we tested it.

3. Concurrency limits

Every Browserless plan caps how many browsers you can have connected at once:

PlanConcurrent browsers (monthly / annual billing)Max session time
Free22 minutes
Prototyping5 / 1015 minutes
Starter30 / 4030 minutes
Scale80 / 10060 minutes
  • One worker, one browser. Set CONCURRENCY to your plan's limit or a little below it, so a reconnecting worker doesn't push you over.
  • Above the limit, new connections wait or are refused. The connect() function treats a failed connection as "busy, try again" with backoff of up to 30 seconds, instead of failing the run.
  • Use tabs sparingly. You can open several pages inside one connected browser, which doesn't use more connections. Each tab still uses memory on the remote browser, though, and a crash takes all of them down. Two or three tabs per browser is a reasonable ceiling.

4. The four timeouts that prevent disconnects and wasted units

TimeoutWhere it's setWhat it protects against
Connectionconnect_over_cdp(..., timeout=60_000)Hanging forever when the service is busy or unreachable. Playwright's default is 30 seconds.
Session cap&timeout=600000 in the WebSocket URL, in milliseconds, up to your plan's maximum session timeA forgotten or stuck session billing for the full plan maximum. When the cap is reached, the browser closes and your connection drops.
Reconnect pointPAGES_PER_SESSIONHitting the session cap in the middle of a page. Pick a number of pages that finishes well inside the cap: 50 pages at 6 seconds is 5 minutes, inside a 10-minute cap.
Per pagepage.set_default_timeout(30_000)One slow page holding the whole session. It fails, goes back on the queue and the worker reconnects.

After any failed page the worker closes the session and opens a new one, because a page error is often the first sign that the connection has gone. In our test, killing the browser mid-run failed the in-flight pages, the workers retried their connections until the browser came back, and every page was fetched. A page that fails three times, like a 404 that Chrome refuses to load, is written out as an error instead of looping forever.

5. What it costs: Browserless vs a VPS cluster

How Browserless bills

One unit is up to 30 seconds of browser time per connection. Longer sessions use one more unit per 30 seconds, and every reconnect starts a new unit. So the same work can cost very different amounts:

  • A new session per page: at least 1 unit per page, even if the page takes 4 seconds.
  • Reused sessions: 50 pages at 6 seconds is 300 seconds, or 10 units for 50 pages, about 0.2 units per page.
PlanBilled monthlyBilled annually (per month)Units per monthExtra units
Free$01,000None
Prototyping$35$2520,000$0.0020 each
Starter$200$140180,000$0.0017 each
Scale$500$350500,000$0.0015 each

Prices from Browserless' pricing page, October 2026. Residential proxies through Browserless cost 6 units per MB, so a 1 MB page through their proxy uses more units in bandwidth than in browser time. From the Prototyping plan up you can bring your own proxies instead.

The VPS side

Based on typical headless Chromium usage, plan for roughly 300 to 500 MB of RAM per concurrent browser with one or two tabs, plus about one vCPU per three or four busy browsers. That's our estimate. Heavy pages need more. DigitalOcean's Basic Droplets cost $24 a month for 4 GB and 2 vCPUs, $48 for 8 GB and 4 vCPUs, and $96 for 16 GB and 8 vCPUs. Vultr's Cloud Compute plans are in the same range, billed by the hour. On top of the server you maintain Chromium, Playwright, crash recovery and monitoring. Our self-hosted Playwright cluster guide walks through that setup.

Two example workloads

Both assume 6 seconds of browser time per page and a nightly batch that has to finish in about two hours. Maintenance hours are our estimates, priced at $50 an hour.

WorkloadBrowserless, reused sessionsBrowserless, one session per pageVPS cluster
100,000 pages a month (about 3,300 a night, 5 browsers)About 20,000 units: Prototyping, about $35 plus a little overage100,000 units: Starter, $2004 GB Droplet $24 + 2 h upkeep = about $124
500,000 pages a month (about 16,700 a night, 15 browsers)About 100,000 units: Starter, $200500,000 units: Scale, $50016 GB Droplet $96 + 3 h upkeep = about $246
Monthly cost: hosted Chrome vs your own VPS
Monthly cost: hosted Chrome vs your own VPS100k pages, reused sessions: 35 $/month; 100k pages, session per page: 200 $/month; 100k pages, 4 GB VPS + 2 h: 124 $/month; 500k pages, reused sessions: 200 $/month; 500k pages, session per page: 500 $/month; 500k pages, 16 GB VPS + 3 h: 246 $/monthBrowserless, reused sessionsBrowserless, session per pageVPS + upkeep100k pages, reused sessions: 35 $/month100k pages, reused sessions$35100k pages, session per page: 200 $/month100k pages, session per page$200100k pages, 4 GB VPS + 2 h: 124 $/month100k pages, 4 GB VPS + 2 h$124500k pages, reused sessions: 200 $/month500k pages, reused sessions$200500k pages, session per page: 500 $/month500k pages, session per page$500500k pages, 16 GB VPS + 3 h: 246 $/month500k pages, 16 GB VPS + 3 h$246
Browserless at monthly list prices, without proxies. VPS figures use DigitalOcean list prices plus our estimate of maintenance time at $50 an hour. Annual Browserless billing is 30% cheaper.

The main lesson is that connection strategy matters more than vendor choice. Reusing sessions makes hosted Chrome cheaper than a VPS at both volumes once your time counts. Opening a session per page makes it the most expensive option. If your time costs nothing and the browsers run around the clock, a VPS stays flat while units keep adding up, so very high, steady volumes favour your own servers.

6. Which one to pick

Your situationBest fit
Bursty batch jobs, existing Playwright code, no one to babysit serversBrowserless with reused sessions
Scrapers running in serverless functions or CI, where you can't install ChromeBrowserless
Browsers busy around the clock, a team that already runs serversYour own VPS cluster on Vultr or DigitalOcean
Protected sites where you only need the rendered HTMLA scraping API. See ZenRows vs self-hosted Playwright

7. Tools

Top picks

ToolRoleWhy it fits
BrowserlessHosted Chrome for Playwright and PuppeteerSwap launch() for connect_over_cdp() and your scripts run on managed browsers in the US or Europe. Per-30-second units reward session reuse, and the free plan with 1,000 units is enough to test your scripts.
VultrVPS for your own browser clusterHourly-billed cloud servers in many regions. Handy for spinning up extra high-RAM servers for a big batch and deleting them afterwards.
DigitalOceanVPS for your own browser clusterPredictable Droplet pricing, from $24 for 4 GB to $96 for 16 GB, and clear docs for running Docker-based Chromium workers.

Alternatives: free tools

We don't earn anything from these links.

8. Pitfalls

  • One connection per page. The easiest code to write and the most expensive to run. Reuse sessions.
  • Not closing the browser. An open session keeps using units until the timeout. Close it in a finally block.
  • More workers than your plan allows. Extra connections queue or fail, and your run slows down instead of speeding up.
  • Proxy bandwidth through the service. At 6 units per MB, residential traffic can cost more than the browser time. Block images and fonts, or bring your own proxy.
  • Chatty scripts over the network. Hundreds of tiny calls per page add latency. Batch them into one page.evaluate().
Bottom line

Moving Playwright to hosted Chrome is a one-line change: connect_over_cdp() instead of launch(). Whether it saves money depends on how you connect. Reuse each session for dozens of pages, stay under your plan's concurrency, reconnect before the session cap and always close the browser, and Browserless costs less than a high-RAM VPS plus the hours to run it. Open a session per page, or run browsers around the clock, and your own Vultr or DigitalOcean servers win.

David Lewin
David Lewin

Founder of StealthScale.io and e-commerce infrastructure engineer writing field-tested playbooks on proxies, cloud VPS, anti-detect browsers and SEO.