1.One line changes, the rest of the script does not
Playwright and Puppeteer both separate launching a browser from driving one. Locally you call launch, which starts a process and hands back a handle. Against a remote browser you call connectOverCDP or connect with a WebSocket URL, and you get back the same kind of handle.
Everything downstream — newPage, goto, fill, click, waitForSelector, evaluate — behaves identically, because CDP is the protocol the local case uses too. The difference is only whether the socket goes to a process on your machine or across the network.
This is what makes migration cheap for teams that already have working automation. You are not rewriting a script into someone's DSL; you are changing where the browser lives.
123456789101112131415161718192021import { chromium } from "playwright";
const wsUrl =
"wss://browser.omniscrape.io" +
`?apikey=${process.env.OMNISCRAPE_KEY}` +
"&proxy_country=de" +
"&render_media=false";
const browser = await chromium.connectOverCDP(wsUrl);
try {
const page = await browser.newPage();
await page.goto("https://example.com/login");
await page.fill("#email", process.env.LOGIN_EMAIL);
await page.fill("#password", process.env.LOGIN_PASSWORD);
await page.click("button[type=submit]");
await page.waitForSelector(".dashboard");
console.log(await page.title());
} finally {
// The meter runs until this socket closes. Always in finally.
await browser.close();
}
123456789101112131415161718import os
from playwright.sync_api import sync_playwright
ws = (
"wss://browser.omniscrape.io"
f"?apikey={os.environ['OMNISCRAPE_KEY']}"
"&proxy_country=us"
"&render_media=false"
)
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(ws)
try:
page = browser.new_page()
page.goto("https://example.com")
print(page.title())
finally:
browser.close()
2.When a remote browser is the right tool
Use it when you need a browser that stays open across many steps. Full login flows including second-factor prompts, multi-page wizards, anything where later steps depend on state accumulated by earlier ones — these need a live session, not a sequence of independent fetches.
Use it when you already own working automation. A Puppeteer script that has been debugged against a real site represents real knowledge about that site, and moving the browser is far cheaper than reimplementing it as API calls.
Use it when one session should drive many pages. Logging in once and then walking two hundred pages inside that session is both cheaper and more reliable than re-authenticating per page, because the session cookies and the IP stay put.
- Full login flows, including 2FA prompts
- Multi-step wizards where state carries between steps
- Existing Playwright or Puppeteer scripts you do not want to rewrite
- One authenticated session driving many pages
3.When a per-request API is cheaper
A browser-minute is expensive relative to a fetch. OmniScrape bills BaaS at $0.02 per minute standard and $0.10 per minute with media loaded, while a Web Unlocker request is $0.0035. If all you need is a page's content, holding a browser open to get it is roughly the cost of six requests per minute of connection.
Fetching content, rendering a single-page app to read it, and even basic interactions have cheaper paths. Web Unlocker with mode js_rendering renders SPAs, and js_actions scripts clicks, fills, and waits without a persistent session. Those cover a large share of what people initially reach for a browser to do.
The dividing line is state. If the work is a sequence of independent page reads, per-request billing wins. If the work is one continuous session where step twelve depends on step three, a remote browser wins, and it is not close in either direction.
4.Time-based billing changes how you write the script
The meter starts when the WebSocket connects and stops when it closes. That makes connection lifetime a cost variable in a way that per-request billing never is, and it has a direct consequence: an uncaught exception that leaves the socket open keeps billing until something times out.
So browser.close() belongs in a finally block, always. This is the single most common and most expensive mistake with time-billed browsers — a script that throws on a selector change at 2 a.m. and holds a connection open until morning. The pattern costs one line and removes the entire failure mode.
Keep render_media false unless images genuinely matter to what you are doing. It is a five-fold rate difference — $0.02 against $0.10 per minute — and most automation reads text, fills fields, and clicks controls, none of which need images decoded. Turn it on for visual regression work or when a site gates behaviour on image loading, and leave it off otherwise.
5.Identity and reconnects
Pass session_id on the WebSocket URL to pin the same residential IP across reconnects. This matters for flows that survive a dropped connection: without it, reconnecting lands you on a different IP, which invalidates anti-bot cookies bound to the previous one and typically forces you back through a challenge or a login.
Use proxy_country to match the market whose content you need. Sites frequently serve different catalogues, pricing, and availability by geography, and a session in the wrong country returns a coherent page with the wrong data — a failure that passes every technical check you have.
Treat one session as one logical unit of work. Log in, do the work, close. Holding a connection open between units to avoid re-authenticating is a false economy on a per-minute meter — idle connection time bills at exactly the same rate as working time.
6.Connection errors and concurrency
Failures surface as a rejected WebSocket upgrade rather than as a page error, so the message from Playwright or Puppeteer is what carries the status code. A 401 means the apikey is missing or invalid. A 402 means balance is too low, the trial expired, or the plan's concurrent browser limit was reached. A 429 means the concurrency limit specifically, and should be retried with backoff.
Concurrent browsers are capped by plan, the same way scrape concurrency is. A worker pool sized to your plan is more predictable than opening connections optimistically and handling rejections, because every rejected connection still costs you a round trip and a retry decision.
Wrap connection establishment in the same retry logic you would use for any network call, and make sure the retry path cannot leave an earlier socket open. A retry loop that reconnects without closing the previous attempt is the second most expensive mistake in this category.
Frequently asked questions
Do I have to rewrite my Playwright script?
No. Replace chromium.launch with chromium.connectOverCDP and a WebSocket URL — or puppeteer.launch with puppeteer.connect and browserWSEndpoint. Everything after that line is unchanged, because CDP is the same protocol the local case uses.
When is Web Unlocker cheaper than a remote browser?
Whenever the work is stateless page reads. A request is $0.0035 against $0.02 per browser-minute, and mode js_rendering renders SPAs while js_actions handles clicks and fills. Reach for a browser when later steps depend on state from earlier ones.
Why does my bill look higher than the time I spent working?
Almost always a socket left open by an uncaught error. The meter runs from connect to close, so a script that throws before browser.close() keeps billing. Put the close in a finally block — that one line removes the failure mode entirely.
What does render_media actually change?
It loads images, fonts, and video, and moves the rate from $0.02 to $0.10 per minute. Most automation reads text and clicks controls, none of which needs images decoded, so leave it off unless you are doing visual work or the site gates behaviour on image loading.
How do I keep the same IP if my connection drops?
Pass session_id on the WebSocket URL. It pins the same residential IP across reconnects, so anti-bot cookies bound to that IP survive the reconnect instead of forcing you back through a challenge or a login.
Related guides
- Puppeteer Web Scraping: Patterns, Anti-Bot Limits, and BaaS Integration
- Playwright Web Scraping: Practical Patterns for Protected Sites
- Headless Browser Scraping: When to Use It and How to Do It Right
- Selenium Web Scraping: Practical Patterns for Real-World Projects
- Scrapy Web Scraping with OmniScrape: Download Middleware, Pipelines, and Scale
- Beautiful Soup Web Scraping: A Practical Guide