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.
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.
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.
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:
| Plan | Concurrent browsers (monthly / annual billing) | Max session time |
|---|---|---|
| Free | 2 | 2 minutes |
| Prototyping | 5 / 10 | 15 minutes |
| Starter | 30 / 40 | 30 minutes |
| Scale | 80 / 100 | 60 minutes |
- One worker, one browser. Set
CONCURRENCYto 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
| Timeout | Where it's set | What it protects against |
|---|---|---|
| Connection | connect_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 time | A forgotten or stuck session billing for the full plan maximum. When the cap is reached, the browser closes and your connection drops. |
| Reconnect point | PAGES_PER_SESSION | Hitting 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 page | page.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.
| Plan | Billed monthly | Billed annually (per month) | Units per month | Extra units |
|---|---|---|---|---|
| Free | $0 | 1,000 | None | |
| Prototyping | $35 | $25 | 20,000 | $0.0020 each |
| Starter | $200 | $140 | 180,000 | $0.0017 each |
| Scale | $500 | $350 | 500,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.
| Workload | Browserless, reused sessions | Browserless, one session per page | VPS cluster |
|---|---|---|---|
| 100,000 pages a month (about 3,300 a night, 5 browsers) | About 20,000 units: Prototyping, about $35 plus a little overage | 100,000 units: Starter, $200 | 4 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, $200 | 500,000 units: Scale, $500 | 16 GB Droplet $96 + 3 h upkeep = about $246 |
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 situation | Best fit |
|---|---|
| Bursty batch jobs, existing Playwright code, no one to babysit servers | Browserless with reused sessions |
| Scrapers running in serverless functions or CI, where you can't install Chrome | Browserless |
| Browsers busy around the clock, a team that already runs servers | Your own VPS cluster on Vultr or DigitalOcean |
| Protected sites where you only need the rendered HTML | A scraping API. See ZenRows vs self-hosted Playwright |
7. Tools
Top picks
| Tool | Role | Why it fits |
|---|---|---|
| Hosted Chrome for Playwright and Puppeteer | Swap 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. | |
| VPS for your own browser cluster | Hourly-billed cloud servers in many regions. Handy for spinning up extra high-RAM servers for a big batch and deleting them afterwards. | |
| VPS for your own browser cluster | Predictable 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.
- Playwright connect_over_cdp docs: the full option list for the connection.
- Browserless open-source image: Browserless publishes its browser server's source on GitHub. Check its license before using it commercially.
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
finallyblock. - 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().
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.