🎯 A customizable, anti-detection cloud browser powered by self-developed Chromium designed for web crawlers and AI Agents.👉Try Now
Back to Blog

Bright Data Review and Alternatives: Migrating Your Scrapers

James Thompson
James Thompson

Scraping and Proxy Management Expert

11-Aug-2026

TL;DR:

  • Bright Data routes two different products through one endpoint. Web Unlocker and SERP API both POST to https://api.brightdata.com/request; only the zone in the request body decides which one answers. Porting therefore starts by demultiplexing your zones, not by swapping a hostname.
  • Three things break in a naive port, and none of them raise a helpful error: the auth header changes name, the response arrives as a JSON envelope instead of raw markup, and the two surfaces sit on different API versions.
  • Browser automation is the easy leg. Both products speak CDP, so Playwright and Puppeteer code survives a changed connection URL. The credentials move from the URL's userinfo to a query parameter.
  • There is a deadline forcing the question anyway. Bright Data's own FAQ states the certificates on proxy ports 22225 and 33335 expire on 25 September 2026, and callers must move to port 44445. Every integration is getting opened before then.
  • Start free: the Scrapeless dashboard issues a key that works against every surface described here.

What Bright Data Is

Bright Data sells web-data collection as a set of separately-branded products sitting on one proxy network. Three of them matter when you are moving scraper code:

  • Web Unlocker API — you hand it a URL, it handles proxy rotation, fingerprints and challenges, and returns the page.
  • SERP API — the same request pipeline aimed at search engines, where you pass a fully-built search URL.
  • Browser API — a hosted Chrome you drive over the Chrome DevTools Protocol with Playwright, Puppeteer or Selenium.

The important structural fact, and the one that shapes every migration: the first two are the same HTTP endpoint. Per Bright Data's Web Unlocker documentation and its SERP API documentation, both products accept a POST to https://api.brightdata.com/request carrying zone, url and format. The zone value is what routes the call.

That is convenient while you are on Bright Data and awkward when you leave, because your codebase probably has one request helper whose behaviour depends on a configuration string.

Key Features

Across those three products the request surface is small and consistent:

  • Authentication is a single account API key sent as Authorization: Bearer <key>.
  • Routing is by zone, configured in the dashboard rather than in the request.
  • format: "raw" returns the target's response body directly.
  • Geo-targeting, session pinning and mobile user agents are expressed inside the proxy username, using suffixes such as -country-<code> and -session-<id>.
  • The Browser API connects at wss://<username>:<password>@brd.superproxy.io:9222, per Bright Data's Browser API configuration reference.

Products and Pricing

Bright Data prices per product and per zone, so a single account commonly carries several zones with separate rates and separate limits. That model is the reason a migration is rarely a one-line change: the zone doubles as the billing and configuration boundary, so moving off it touches cost attribution as well as routing.

Scrapeless prices by request against one key, with the surface chosen by the endpoint you call rather than by a dashboard object. Current figures for both sides live on their respective pricing pages; this guide deliberately maps mechanics rather than quoting rates that move.

Performance and Fit

Bright Data's proxy network is large and its unlocking success rate on hard targets is the reason most teams adopt it in the first place. Nothing in this guide argues that the product does not work.

What moves teams is usually structural rather than technical: per-zone configuration that drifts, billing that is hard to attribute back to a specific job, and the operational overhead of keeping several zones aligned. Those are the conditions under which "port the code" becomes a real question.

The Scrapeless Alternative

Scrapeless splits the same job across surfaces that are chosen by URL rather than by configuration:

  • Universal Scraping APIPOST https://api.scrapeless.com/api/v2/unlocker/request for unblocked page fetches.
  • Scraper APIPOST https://api.scrapeless.com/api/v1/scraper/request with an actor naming the target surface, answering inline with parsed fields.
  • Scraping Browser — a CDP endpoint at wss://browser.scrapeless.com/api/v2/browser, with the key passed as a token query parameter.

One key covers all three. There is no zone object, which removes the dashboard round-trip but also means the demultiplexing has to happen in your code during the port.

Where Bright Data Still Has the Edge

Three things are genuinely easier to do on Bright Data than after a port:

  • The zone model genuinely is convenient when many jobs share one call site and you want to change behaviour without a deploy.
  • Selenium is supported against the Browser API. The Scrapeless Scraping Browser is CDP-only, so a Selenium-based suite needs rewriting onto Playwright or Puppeteer rather than repointing.
  • Proxy-username modifiers such as -country- and -session- express geo and session pinning without touching the request body, which some codebases lean on heavily.

Where the Model Costs You

  • One endpoint, two products means static analysis cannot tell you what a given call actually does without resolving the zone.
  • Configuration lives outside the repository, so a zone change is invisible to code review and to git blame.
  • Port and certificate churn lands on you. Bright Data's general FAQ states that the old certificates on ports 22225 and 33335 expire on 25 September 2026 at 00:00 UTC, and that callers still on those ports must complete the move to port 44445 before that date.

Implementation: Porting the Code

This is the part other migration write-ups skip. Each subsection below is a real difference that produces a wrong result rather than a clear error.

The Endpoint and Parameter Map

Concern Bright Data Scrapeless
Unblocked page fetch POST https://api.brightdata.com/request · {"zone","url","format":"raw"} POST https://api.scrapeless.com/api/v2/unlocker/request · {"actor":"unlocker.webunlocker","input":{"url","js_render"}}
Search results the same endpoint, SERP zone · url is a pre-built search URL POST https://api.scrapeless.com/api/v1/scraper/request · {"actor":"scraper.google.search","input":{"q","gl","hl"}} · returns parsed results inline
Browser automation wss://<user>:<pass>@brd.superproxy.io:9222 wss://browser.scrapeless.com/api/v2/browser?token=<key>
Auth Authorization: Bearer <key> x-api-token: <key>
Product routing zone in the body the endpoint plus actor
Success body the target's response body JSON envelope; markup inside data

Breakage 1: The Auth Header Changes Name

The most common failed first attempt swaps the host and the key but keeps the header. Bright Data uses Authorization: Bearer; Scrapeless uses x-api-token. A request carrying only the old header is unauthenticated, and the failure looks like a credential problem rather than a header-name problem.

bash Copy
# Bright Data — illustrative, from the vendor's published example
curl -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${BRIGHTDATA_API_KEY}" \
  -d '{"zone":"YOUR_ZONE_NAME","url":"https://example.com","format":"raw"}' \
  https://api.brightdata.com/request
bash Copy
# Scrapeless — the same intent
curl -sS -X POST "https://api.scrapeless.com/api/v2/unlocker/request" \
  -H "x-api-token: ${SCRAPELESS_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"actor":"unlocker.webunlocker","input":{"url":"https://example.com","js_render":false}}'

Breakage 2: The Response Is an Envelope

With format: "raw" Bright Data hands back the target's body, so callers commonly do response.text and parse it. Scrapeless answers with a JSON object and puts the markup in data.

A port that changes only the URL and the header will parse a JSON string as if it were HTML. Selectors return nothing, no exception is raised, and the job records empty results. The fix is one line, but only if you know to make it.

python Copy
import os
import requests

KEY = os.environ["SCRAPELESS_API_KEY"]

def fetch(url: str, js_render: bool = False) -> str:
    """Return page HTML. The envelope is unwrapped here, once."""
    response = requests.post(
        "https://api.scrapeless.com/api/v2/unlocker/request",
        headers={"x-api-token": KEY, "Content-Type": "application/json"},
        json={"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": js_render}},
        timeout=90,
    )
    response.raise_for_status()
    body = response.json()
    # Bright Data with format:"raw" would have given you this directly as response.text
    return body["data"]

html = fetch("https://example.com")
print(f"chars={len(html)} starts_with_doctype={html.lstrip().lower().startswith('<!doctype')}")

Keep the unwrap in one helper. Scattering response.json()["data"] across call sites is how half a codebase gets ported and the other half quietly returns JSON.

Breakage 3: The Two Surfaces Sit on Different API Versions

This one catches people who assume a provider has one API. The unlocker surface is on /api/v2/; the Google search actor is on /api/v1/. Posting the search actor to the v2 path returns HTTP 400 {"message":"unknown task type"} — a message that reads like a malformed body and sends you off inspecting your input fields, when the version segment is the actual problem.

The search call itself is synchronous. It answers in one round trip with parsed results, so there is no task identifier and no poll loop.

python Copy
import os
import requests

KEY = os.environ["SCRAPELESS_API_KEY"]

def search(query: str, gl: str = "us", hl: str = "en") -> dict:
    """Google SERP as structured JSON. Note the v1 path — the unlocker is v2."""
    response = requests.post(
        "https://api.scrapeless.com/api/v1/scraper/request",
        headers={"x-api-token": KEY, "Content-Type": "application/json"},
        json={"actor": "scraper.google.search", "input": {"q": query, "gl": gl, "hl": hl}},
        timeout=120,
    )
    response.raise_for_status()
    return response.json()

data = search("web scraping api")
print("sections:", sorted(data))
for row in data.get("organic_results", [])[:3]:
    print(f"  {row['position']}. {row['title'][:60]}")
    print(f"     {row['link']}")

The larger change here is what comes back. Bright Data's SERP API with format: "raw" hands you the search engine's HTML and you parse it yourself. Scrapeless returns the page already parsed — a live call for web scraping api came back with organic_results, pagination, related_searches, search_information and metadata, where each organic row carries position, title, link, snippet, source, favicon, redirect_link and snippet_highlighted_words.

Two consequences for the port. Your SERP HTML parser becomes dead code — delete it rather than porting it. And any query-string assembly you own becomes dead too, because the query and locale move into input as q, gl and hl instead of being baked into a URL.

Breakage 4: Browser Credentials Move Position

Both products expose CDP, so the automation code itself carries over. What changes is where the credential sits — Bright Data puts it in the URL's userinfo, Scrapeless in a query parameter.

python Copy
import os
from playwright.sync_api import sync_playwright

# Bright Data (illustrative):
#   wss://<username>:<password>@brd.superproxy.io:9222
endpoint = f"wss://browser.scrapeless.com/api/v2/browser?token={os.environ['SCRAPELESS_API_KEY']}"

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(endpoint)
    page = browser.new_page()
    page.goto("https://quotes.toscrape.com/", wait_until="domcontentloaded")
    print("title:", page.title())
    print("quotes on page:", len(page.query_selector_all(".quote")))
    browser.close()

This position change has an operational consequence worth planning for. Many logging and tracing libraries strip URL userinfo automatically but leave query strings intact, so a connection string that was previously redacted by default may start appearing in logs. Filter on the token parameter before you ship.

Two constraints to size before you start: the Scrapeless Scraping Browser speaks CDP only, so a Selenium suite is a rewrite rather than a repoint, and connect_over_cdp attaches to a remote browser rather than launching one, so any launch() arguments in your current code have no destination.

A Migration Order That Works

  1. Inventory your zones and label each one unlocker-shaped or SERP-shaped. This is the demultiplexing step and it is the only genuinely manual part.
  2. Port the unlocker path first — it is a header change plus the envelope unwrap, in one helper.
  3. Port the SERP path second, switching to the v1 path and deleting both your URL assembly and your HTML parser.
  4. Repoint browser automation last; it is the smallest diff.
  5. Run both stacks against the same URL list and diff the extracted fields, not the raw bytes. Markup differs harmlessly between fetches; extracted values should not.

Use Cases That Migrate Cleanly

  • Price and catalogue monitoring — unlocker-shaped, one helper, the highest-volume and easiest leg.
  • Rank tracking and SERP monitoring — gains structured fields, loses the URL assembly.
  • Agent and RAG retrieval pipelines — the parsed SERP output drops straight into a retrieval step, removing a parsing stage rather than adding one.

The case that does not migrate cleanly is a Selenium-based browser suite, for the reason given above. Budget that separately rather than folding it into the same sprint.

Pricing Comparison

The honest comparison is structural rather than numeric. Bright Data attributes cost to a zone, so spend is grouped by configuration object and a job that spans two zones appears in two places. Scrapeless attributes cost to requests against one key, so attribution follows the code path that made the call.

If you are migrating partly for cost visibility, that difference matters more than the headline rate. Check current numbers on the Scrapeless pricing page and on Bright Data's own pricing page before modelling anything.

Conclusion

A Bright Data migration is small in diff size and easy to get subtly wrong. The endpoint and the key are the visible half; the half that costs a debugging cycle is that one Bright Data endpoint maps to two Scrapeless surfaces, that the body arrives wrapped, and that those two surfaces sit on different API versions. Port the unlocker path first, keep the envelope unwrap in a single helper, and diff extracted fields rather than raw HTML when you cut over.

If your integration is still on ports 22225 or 33335, you have a deadline on the calendar regardless of which provider you end up on. That is a reasonable moment to decide deliberately rather than under time pressure in late September.

Ready to Move Your Scrapers?

Create a key on the Scrapeless dashboard and run the unlocker snippet above against one URL you already collect. Comparing the extracted fields against your current output is a fifteen-minute test that answers most of the migration question.

FAQ

Q: How long does a Bright Data migration actually take?

For an unlocker-only integration, a few hours: a header change, an envelope unwrap in one helper, and a diff run. A SERP path is usually faster than expected, because you delete more code than you write — the HTML parser and the URL assembly both go. A Selenium browser suite is the outlier and should be scoped on its own.

Q: Do I need to recreate my zones?

No — there is no zone object to recreate. One key covers every surface. The work moves into your code instead: each zone has to be classified as unlocker-shaped or SERP-shaped so the call site knows which endpoint to hit.

Q: Will my Playwright or Puppeteer code still work?

Yes. Both providers expose a CDP WebSocket, so the automation body carries over unchanged and only the connection string differs. Selenium is the exception: the Scrapeless Scraping Browser is CDP-only.

Q: Why does my ported code return empty results without raising an error?

Almost always the response envelope. Bright Data with format: "raw" returns the page body, while Scrapeless returns JSON with the markup inside data. Parsing the envelope as HTML yields no matches and no exception. Change response.text to response.json()["data"].

Q: What happens on 25 September 2026?

Bright Data's FAQ states the certificates on ports 22225 and 33335 expire at 00:00 UTC that day, and that traffic must move to port 44445. That applies to staying on Bright Data as much as to leaving, so treat it as a scheduling constraint rather than an argument either way.

Q: Can I run both providers at once during the cutover?

Yes, and it is the recommended approach. Keep both paths behind one interface, send a sample of traffic to each, and compare extracted fields. Cut over per job rather than all at once, starting with the highest-volume unlocker workload.

At Scrapeless, we only access publicly available data while strictly complying with applicable laws, regulations, and website privacy policies. The content in this blog is for demonstration purposes only and does not involve any illegal or infringing activities. We make no guarantees and disclaim all liability for the use of information from this blog or third-party links. Before engaging in any scraping activities, consult your legal advisor and review the target website's terms of service or obtain the necessary permissions.

Most Popular Articles

Catalogue