A team migrating a 200 worker scraping fleet from Selenium to Playwright once told me their block rate dropped by a third in the first week. They credited Playwright. The actual cause was more mundane: their Selenium setup had been pinning one proxy per browser process at launch, so every tab in that process shared an exit IP and every session carried the same identity for hours. Playwright's per-context proxy option let them bind one IP to one browser context, and the target site stopped seeing a dozen "users" with identical network origins.
That story captures why this comparison matters. Playwright and Selenium both drive real browsers, both render JavaScript, both accept a proxy. The difference that shows up in production is not which framework clicks buttons faster. It is how precisely each one lets you bind a network identity to a browsing session, how much it leaks about being automated, and how much bandwidth it burns per page when that bandwidth is metered residential traffic.
This article compares the two on the three axes that decide the outcome of a scraping programme: proxy configuration mechanics, stealth surface, and performance economics at scale.
The Architectural Split That Explains Everything Else
Selenium implements the W3C WebDriver specification. Your script talks HTTP to a driver binary (chromedriver, geckodriver), which talks to the browser through a vendor specific channel. The specification was written for cross-browser functional testing, not for data collection, and it has no concept of a proxy credential, a request interceptor, or an isolated browsing identity. Anything beyond the basics has to be bolted on through browser command line flags, profile manipulation, or raw Chrome DevTools Protocol calls.
Playwright takes a different route. It ships patched browser builds and speaks a single persistent WebSocket connection to the browser, driving it through CDP for Chromium and through equivalent protocols for Firefox and WebKit. Because Playwright controls the build and the protocol, it can expose browser internals that WebDriver never standardised: request interception, per-context network settings, credential handling, storage isolation.
That split is the root cause of nearly every practical difference below. Selenium gives you portability and a massive ecosystem. Playwright gives you control over the network layer, which is exactly the layer proxy work lives on.
Proxy Configuration in Practice
Selenium: Flags, Extensions, and Workarounds
The baseline Selenium proxy setup is a Chromium command line flag:
from selenium import webdriveropts = webdriver.ChromeOptions()opts.add_argument("--proxy-server=http://gw.example.net:8000")opts.add_argument("--proxy-bypass-list=<-loopback>")driver = webdriver.Chrome(options=opts)
Two constraints become obvious immediately.
The proxy is process wide. Every tab, every window, every navigation in that browser instance exits through the same upstream. If you want a second IP, you launch a second browser process. On a machine running thirty workers, that is thirty full Chromium processes rather than thirty lightweight contexts inside a handful of processes.
There is no native way to pass credentials. The WebDriver specification has no proxy authentication field, and Chromium will not accept user:pass@host in --proxy-server. Teams work around this in three ways. They whitelist the worker's outbound IP with the provider so the gateway accepts unauthenticated connections, which is clean but inflexible on autoscaling infrastructure where egress IPs change. They generate a tiny Chrome extension on the fly that answers the onAuthRequired event with stored credentials, which works but adds an extension to the browser profile and therefore to the fingerprint surface. Or they use a library that proxies the proxy, running a local relay that injects the Proxy-Authorization header, which adds a hop and, in some implementations, terminates TLS locally and rewrites the handshake in a way that changes your TLS fingerprint.
That last point deserves emphasis because it is the single most common silent failure in Selenium proxy stacks. A local man-in-the-middle relay that re-originates HTTPS will present its own TLS client hello, not Chromium's. You can have a perfect residential IP and still get flagged because the handshake no longer looks like the browser it claims to be.
The IP whitelisting route avoids all of it. If your workers run on stable egress addresses, authenticate by IP, pass the gateway plainly in --proxy-server, and keep the browser's own network stack intact.
Firefox under Selenium is slightly more flexible because proxy settings live in the profile and can be set through preferences, including SOCKS with remote DNS:
from selenium import webdriverprofile = webdriver.FirefoxOptions()profile.set_preference("network.proxy.type", 1)profile.set_preference("network.proxy.socks", "gw.example.net")profile.set_preference("network.proxy.socks_port", 1080)profile.set_preference("network.proxy.socks_remote_dns", True)driver = webdriver.Firefox(options=profile)
Still process wide, still no credential field, but at least DNS resolution is explicitly routed through the proxy rather than left to the host resolver.
Playwright: Per Context Proxies and Built In Auth
Playwright treats the proxy as a first class option at both the browser and the context level:
from playwright.sync_api import sync_playwrightwith sync_playwright() as p: browser = p.chromium.launch() ctx = browser.new_context( proxy={ "server": "http://gw.example.net:8000", "username": "customer-acme-session-7f3a-country-de", "password": "••••••", }, locale="de-DE", timezone_id="Europe/Berlin", viewport={"width": 1440, "height": 900}, ) page = ctx.new_page() page.goto("https://example.de/catalog")
Three things that were painful in Selenium are trivial here. Credentials are a native field, so no extension and no local relay. The proxy is scoped to a browser context, so one Chromium process can host many contexts, each with its own exit IP, cookie jar, local storage, and cache. And the context options let you align locale, timezone, geolocation, and viewport with the exit node's country in the same constructor call, which is the difference between an IP that looks German and a browser profile that looks German.
That last point is underrated. Most residential proxy gateways encode session control in the username: a session token for stickiness, a country or city parameter for geo-targeting. Playwright lets you build that username per context, which means your rotation logic and your fingerprint logic live in the same code path. A context that resolves to Munich gets de-DE, Europe/Berlin, and a German Accept-Language header together, every time, because they are assembled from one config object.
For rotation, the pattern is simple: create a context, run a bounded unit of work, close the context, create a new one with a fresh session token. Contexts are cheap. A Chromium process with twenty contexts uses far less memory than twenty Chromium processes, and context teardown guarantees that no cookie or cache entry crosses an identity boundary.
One caveat worth knowing: on Chromium, per-context proxying works reliably when the browser is launched with a proxy configured (even a dummy one) so the network service is initialised in proxy-aware mode. Playwright's documentation covers the exact behaviour per browser engine, and it is worth testing your specific version rather than assuming.
Rotation Granularity and Session Mapping
The practical question is not "can I set a proxy" but "how small is the unit I can bind an IP to".
With Selenium, the unit is a browser process. A sensible architecture is a pool of short-lived browser processes, each launched with a different gateway session, each handling a batch of pages before being recycled. This works, and plenty of large scrapers run exactly this way. The cost is process churn: Chromium cold start is several hundred milliseconds to a couple of seconds depending on flags and disk, and memory spikes during startup are what take nodes down when concurrency is tuned too aggressively.
With Playwright, the unit is a context, and the cost of creating one is measured in low tens of milliseconds. That lets you do things that are impractical in Selenium: one IP per product page on a hostile target, one IP per checkout simulation, a fresh identity per search query on a SERP-style endpoint.
It also lets you mix pool types inside one process. A worker can hold a long-lived context on a static ISP exit for the authenticated portion of a workflow and spin ephemeral contexts on rotating residential exits for the public catalogue crawling, without running two separate browser fleets.
Where Selenium catches up is Selenium Grid. If your infrastructure already runs Grid, you can assign proxy configuration at the node level, keeping proxy logic out of the test code entirely and letting the scheduler route jobs to nodes in the right geography. For organisations with existing Grid investment and compliance processes around it, that is a legitimate reason to stay.
Stealth: What Each Framework Leaks
Neither framework is invisible, and both leak in similar places. The differences are in degree and in how easy they are to patch.
Automation flags. Chromium exposes navigator.webdriver when driven by WebDriver. Selenium historically triggered it directly, and the --disable-blink-features=AutomationControlled flag plus excludeSwitches is a standard counter. Playwright sets it too, but stealth plugins and patched launch arguments address it in both ecosystems. Treat this as table stakes, not a differentiator. Detection vendors moved past navigator.webdriver years ago.
Driver artefacts. Selenium's chromedriver historically injected identifiable properties into the document and used predictable command patterns. Modern versions are better, but the attack surface is real: an extra binary sitting between your script and the browser, with its own network listener on localhost.
CDP visibility. Playwright's reliance on the DevTools Protocol is detectable in specific circumstances, for example through timing side channels around Runtime.enable or by observing how the page reacts to console object serialisation. The detection techniques exist publicly and some commercial anti-bot vendors use them.
Engine coverage. Playwright ships WebKit and Firefox builds, which means you can diversify beyond Chromium without maintaining three driver stacks. If a target has tuned its detection against Chromium signals, running a share of traffic through a different engine changes the shape of your fleet. Selenium supports those browsers too, but with vendor drivers of varying maturity and far less consistent API behaviour.
Network layer consistency. This is where the proxy choice outranks the framework choice. Both Playwright and Selenium, when they let the browser own the connection, inherit the browser's genuine TLS fingerprint. Both break that property the moment you insert a local MITM relay for proxy auth. Both expose you if WebRTC resolves a local or ISP address that contradicts the proxy exit, or if the host resolver answers DNS instead of the proxy.
That WebRTC point catches teams constantly. A Chromium instance behind an HTTP proxy will still attempt STUN over UDP unless you disable it. In Playwright, launch with WebRTC IP handling locked down; in Selenium, the same flags apply. Then verify, rather than assume, by loading a leak test page through the automated browser itself rather than through a manual check in your normal browser.
The honest summary: Playwright gives you more levers to align browser state with network identity, and that alignment is what modern detection grades you on. Selenium can reach the same place, but you will write more glue code and you are more likely to introduce a fingerprint-breaking workaround along the way.
Performance and the Bandwidth Bill
Browser automation is expensive on metered proxy traffic. A modern e-commerce product page pulls somewhere between 1.5 and 6 MB of assets. At residential per-GB pricing, a million page loads of unfiltered browser traffic is a budget line that gets noticed.
Playwright's request interception is the strongest practical argument in its favour for scraping economics:
ctx.route("**/*.{png,jpg,jpeg,gif,webp,svg,woff,woff2,ttf,mp4}", lambda r: r.abort())ctx.route("**/{analytics,gtm,doubleclick,hotjar}**", lambda r: r.abort())
Two lines of routing rules commonly cut transferred bytes by 60 to 80 percent on content-heavy pages, with a corresponding drop in page load time. Be deliberate about what you block: some sites gate content behind a script you just killed, and aggressive blocking of everything non-HTML can itself become a behavioural signal if the request pattern looks nothing like a real browser session.
Selenium has no native equivalent. You can reach for CDP directly on Chromium:
driver.execute_cdp_cmd("Network.enable", {})driver.execute_cdp_cmd("Network.setBlockedURLs", {"urls": ["*.png", "*.jpg", "*.woff2"]})
It works, it is Chromium-only, and it couples your supposedly browser-agnostic Selenium code to the protocol Selenium exists to abstract away.
On raw concurrency, the memory model decides. Contexts share a browser process in Playwright, so a 16 GB node can hold meaningfully more concurrent identities than the same node running one Chromium process per identity. Playwright's auto-waiting also removes most of the explicit sleep and wait scaffolding that inflates Selenium wall-clock times, which matters when you are paying for compute by the hour and for proxy bandwidth by the gigabyte.
Selenium's advantage is maturity: a decade and a half of Grid tooling, language bindings, CI integrations, and institutional knowledge. If your organisation already runs Selenium Grid with proper observability, the migration cost is not trivial and the throughput gain has to justify it.
Common Mistakes That Sink Proxy Enabled Automation
Binding one proxy to an entire fleet at launch. The scenario from the introduction. Many identities, one network origin. Correlation is trivial for the target.
Rotating faster than sessions survive. If a multi-step flow needs a stable IP for login, cart, and checkout, rotating mid-flow breaks the session and often trips a security review on the account. Match session lifetime to workflow length, not to a fixed timer.
Letting DNS leak around the proxy. With SOCKS5, make sure you are doing remote resolution rather than resolving locally and tunnelling only the connection. With HTTP CONNECT proxies this is handled by the proxy by design, but mixed stacks where some traffic bypasses the tunnel are common and hard to spot.
Forgetting the bypass list. A worker that routes its own health check or localhost calls through a metered residential gateway is burning paid bandwidth on internal traffic.
Reusing contexts or profiles across identities. Cookies, local storage, service worker caches, and IndexedDB all persist. A fresh IP attached to a used profile is not a fresh identity.
Measuring success by HTTP 200. Soft blocks return 200 with an empty or sanitised body. Validate on content assertions, not status codes, or your dashboards will look healthy while your dataset silently degrades.
Where Proxies Fit In: Matching Pool Type to Your Automation Stack
Framework choice determines how precisely you can bind an identity. The proxy pool determines whether that identity is trusted in the first place, and the two decisions have to be made together.
For public catalogue crawling, SERP work, and price collection on consumer-facing sites, rotating residential exits give the IP diversity that keeps per-IP request counts below detection thresholds. For authenticated flows that need an IP to hold steady across a login and several subsequent steps, static ISP addresses behave like residential IPs to reputation systems while delivering datacenter-grade stability. For mobile-first platforms and app APIs, carrier-grade NAT means a mobile exit is shared with real subscribers, which raises the cost of blocking it. For internal targets, staging environments, and your own infrastructure, datacenter IPs remain the sensible and cheapest option. Access to ethically sourced residential proxy pools alongside ISP, mobile, and datacenter options in one account is what makes that mixing practical, because you are not integrating four vendors to run one pipeline.
The operational details matter as much as pool type. Gateway credentials should support session tokens so a Playwright context can request a sticky IP and release it deterministically. Country and city targeting should be parameterised in the username so geo selection stays in your code rather than in a dashboard. And the provider should tolerate the connection pattern browser automation produces: many short-lived TLS connections per page, bursting hard when a fleet scales up. EnigmaProxy positions itself in the professional tier on exactly those points, with multiple pool types, broad geo-coverage, and pricing that lets teams model cost per thousand pages before committing to a volume.
Before a large run, validate the assumptions rather than trusting them. Confirm that the exit country, ASN type, and resolved geolocation match what your context options claim, and that nothing in the stack is rewriting the handshake. A quick pass through a proxy testing tool catches the mismatch between a German locale and a Dutch exit node long before a target's risk engine does.
Strategic Outlook: Where Browser Automation Is Heading
WebDriver BiDi narrows the gap. The bidirectional WebDriver protocol brings event streaming, network interception, and richer browser control into a standard that both frameworks implement. Over the next few release cycles, features that were Playwright exclusives will land in Selenium through BiDi. The architectural advantage will shrink; the ergonomics gap will persist longer.
Detection moves further up the stack. As TLS and IP checks become commodity, vendors are weighting behavioural and environmental consistency more heavily: input timing, scroll dynamics, whether a declared GPU matches the rendered output, whether a session's traffic shape resembles a human's. Framework choice barely affects those signals. Deliberate design of session behaviour does.
Bandwidth becomes a first class design constraint. Teams are increasingly architecting hybrid pipelines: plain HTTP clients for endpoints that do not require rendering, browsers only where JavaScript execution is genuinely necessary. Expect routing logic that decides per URL whether a page deserves a browser and a residential IP or a lightweight request on a cheaper pool.
Autonomous agents inherit all of this. LLM-driven browser agents run longer, less predictable sessions than scripted scrapers, which makes per-session IP stability and clean geo alignment more important rather than less. The infrastructure patterns being refined for scraping today are the ones agent platforms will depend on tomorrow.
Conclusion
If you are starting fresh and the primary goal is data collection at scale, Playwright is the stronger foundation: per-context proxy binding, native credential support, request interception that cuts bandwidth cost, multi-engine coverage, and a resource model that supports higher concurrency per node. If you have an established Selenium Grid, strong operational tooling around it, and a workload where process-level proxy binding is sufficient, the migration may not pay for itself, and Selenium with disciplined browser recycling and IP-whitelisted gateways remains perfectly viable.
What neither framework will do is rescue a weak network layer. Shared, overused, or opaquely sourced IPs will get flagged regardless of how elegant your automation code is, and a local relay added to solve an authentication problem can undo months of fingerprint work in one handshake. Choose the framework for the control it gives you over identity, then pair it with a provider like EnigmaProxy that offers the pool diversity, geo-coverage, and session control your workflow actually requires.