You start with a scraper that works fine on public pages. Then someone asks the agent to log in, open a dashboard, submit a form, and extract the result after an AJAX update. Suddenly your proxy rotation setup starts failing in odd ways: redirects back to /login, 403 after the second page, invalid CSRF tokens, or sessions that work locally but die in production.
The issue usually is not the proxy itself. It is that modern authenticated sites bind state to more than a cookie. They may correlate IP address, browser fingerprint, timezone, locale, localStorage, sessionStorage, and navigation history. If you rotate IPs on every request, you solve one problem and create another.
Proxy rotation and browser sessions solve different problems
Proxy-heavy tools such as Oxylabs and Bright Data are a good fit when you need to fetch public pages at scale. Product listings, public directories, news archives, SERPs, and region-locked static content are common examples. Rotating residential IPs helps distribute traffic and avoid rate limits.
That model gets awkward when the workflow depends on continuity.
A simplified proxy-rotation scrape looks like this:
const urls = [
'https://example.com/account',
'https://example.com/account/billing',
'https://example.com/account/export'
]
for (const url of urls) {
const res = await fetch('https://proxy-api.example/fetch', {
method: 'POST',
body: JSON.stringify({
url,
country: 'US',
rotateIp: true
})
})
console.log(url, res.status)
}
This can fail even if the first request succeeds. The second request may come from a different IP with no matching browser context. The site sees a cookie from one session, an IP from another, and a fingerprint that may not match either. Common symptoms are 401 Unauthorized, 403 Forbidden, a redirect to login, or a page that renders but contains no user-specific data.
Browser-native platforms take the opposite approach. They keep a browser context alive, including cookies, localStorage, headers, viewport, and often timezone or locale. Browserbase, Browserless, Steel, Firecrawl, and similar tools live in this category, though they expose different levels of control.
Wire is an example of the browser-session side of this tradeoff, where authenticated extraction is modeled around persistent sessions and job state rather than one isolated proxy request per URL.
The decision point is state, not vendor category
A simple rule works most of the time:
- Use proxy rotation when the target data is public and each URL can be fetched independently.
- Use browser sessions when the workflow logs in, submits forms, follows multi-page state, or depends on JavaScript-rendered data.
The expensive mistake is buying a larger proxy pool to fix a session problem. More IPs do not help if the site expects the same browser identity across five steps.
Here is the shape of a browser-session workflow:
import { chromium } from 'playwright'
const browser = await chromium.launch()
const context = await browser.newContext({
locale: 'en-US',
timezoneId: 'America/New_York'
})
const page = await context.newPage()
await page.goto('https://example.com/login')
await page.fill('input[name=email]', process.env.EMAIL!)
await page.fill('input[name=password]', process.env.PASSWORD!)
await page.click('button[type=submit]')
await page.waitForURL('**/dashboard')
await page.goto('https://example.com/account/export')
await page.click('text=Generate export')
await page.waitForSelector('[data-status="complete"]')
const downloadUrl = await page.getAttribute('a[data-export]', 'href')
console.log(downloadUrl)
await context.storageState({ path: 'session.json' })
await browser.close()
The important part is not Playwright itself. It is the persisted context. If you load session.json on the next run, you can often skip login entirely:
const context = await browser.newContext({
storageState: 'session.json',
locale: 'en-US',
timezoneId: 'America/New_York'
})
This pattern has edges. Sessions expire. Password changes invalidate stored state. Some sites require MFA every time the IP changes. Long-running browser sessions consume more memory and cost more than plain HTTP requests. If you run hundreds of them concurrently, you need queueing, timeouts, cleanup, and retry logic.
Async job APIs fit browser work better than blocking requests
Browser work is slow compared with fetching HTML. A page may need 5 to 30 seconds to load, run JavaScript, pass bot checks, and render data. If your API blocks until everything finishes, your workers spend most of their time waiting.
A better pattern is submit, poll, fetch result:
async function submitJob(url: string) {
const res = await fetch('https://scraper.example/jobs', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
url,
mode: 'browser',
sessionId: 'acct_123',
country: 'US'
})
})
if (!res.ok) throw new Error(`submit failed: ${res.status}`)
return res.json() as Promise<{ jobId: string }>
}
async function waitForJob(jobId: string) {
while (true) {
const res = await fetch(`https://scraper.example/jobs/${jobId}`)
const job = await res.json()
if (job.status === 'succeeded') return job.result
if (job.status === 'failed') {
throw new Error(job.error ?? 'scrape failed')
}
await new Promise(resolve => setTimeout(resolve, 2000))
}
}
This lets your application submit many browser tasks without tying up one process per page load. It also gives you a clear place to handle failure states such as captcha_required, login_expired, timeout, or blocked.
Wire uses this kind of job-oriented model for browser-based extraction, which matters when several authenticated scraping tasks need to run in parallel and report failure cleanly.
How the common options map out
Oxylabs and Bright Data make sense when IP diversity is the main requirement. Oxylabs publishes residential proxy pricing around bandwidth tiers, with examples like $300/month for 50GB in some plans. That can be reasonable for high-volume static scraping, but it is not designed around preserving login state across arbitrary browser steps.
Browserbase is closer to managed Chromium infrastructure. It fits teams that want Playwright or Puppeteer control without running their own browser fleet. You still need to think about session storage, concurrency, and cost per browser minute.
Firecrawl is useful when the output you want is clean Markdown or structured text from rendered pages. It is a better fit for RAG ingestion than for workflows that need to click through authenticated application state.
Browserless gives you hosted browser automation with its own query layer and browser APIs. Steel is interesting if you want open-source, self-hostable browser infrastructure and you have the ops capacity to run it.
None of these categories is universally better. Static scraping wants throughput and low cost per page. Authenticated agent workflows want continuity and debuggable state.
A practical way to choose
Before comparing pricing pages, classify one real workflow:
- Does every URL stand alone, or does page 3 depend on state from page 1?
- Does the site bind login to IP, device, locale, or MFA?
- Do you need raw browser control, or only extracted text and fields?
- What is the acceptable failure mode: retry, manual re-auth, partial result, or hard fail?
- How many concurrent browser sessions do you actually need?
Then build a small test that runs the same workflow three ways: plain HTTP, HTTP through rotating proxies, and a persistent browser context. Log status codes, redirects, final URL, extracted field counts, runtime, and cost per successful result.
If plain HTTP works, keep it. If proxy rotation works, use it. If either fails after login, stop tuning proxies and test persistent browser sessions instead.
Top comments (0)