Author: Xavier Fok

  • How to Scrape LinkedIn Data Without Getting Banned (2026)

    how to scrape linkedin data without getting banned (2026)

    scraping linkedin without bans in 2026 comes down to four things: residential or mobile proxies (never datacenter), aged accounts with established activity, slow request rates (under 80 actions per day per account), and either a managed scraping api or playwright with anti-detection. linkedin actively detects automation. one ip + one fresh account + 200 requests in an hour = ban within 24 hours. this guide covers the legal context, the technical setup, and how to recover when accounts get restricted.

    we cover legality first, then the proxy stack, account discipline, browser automation, managed api alternatives, and a 2026 ban-recovery playbook.

    is linkedin scraping legal in 2026?

    scraping public linkedin data is generally legal in the us under hiq v linkedin (2022) and follow-on rulings. the courts have repeatedly held that scraping public web data is not a violation of the computer fraud and abuse act.

    scraping linkedin still violates linkedin’s terms of service. tos violations are not criminal but they give linkedin grounds to ban accounts and pursue civil action against commercial scrapers in some cases.

    eu and uk law is stricter. gdpr requires a lawful basis (consent, contract, or legitimate interest) for processing personal data. scraped linkedin data falls under gdpr if it includes eu data subjects. document your lawful basis before processing, and respect data subject rights including erasure requests.

    read linkedin’s user agreement for the current commercial-use restrictions. for a deeper read on the legal landscape around lead-gen scraping see our b2b lead generation proxies guide.

    what gets you banned in 2026

    linkedin’s anti-bot stack flags four signals.

    ip pattern. datacenter ips trigger immediately. shared residential ips with known scraper traffic also flag fast. mobile carrier ips have the longest leash.

    session pattern. login from a new ip with no warm-up history is suspicious. 50 profile views in 10 minutes is a classic bot signal. clicking through every profile from a search result without scrolling looks robotic.

    browser fingerprint. headless chrome without anti-detection patches is detected within minutes. residential proxy + plain selenium = ban in under an hour.

    account age and activity. brand new accounts with zero connections and a thin profile that suddenly perform 500 actions trip every alarm. aged accounts with real history get more leniency.

    beat all four and bans become rare. miss any one and accounts cycle through faster than you can warm them.

    the proxy stack

    mobile proxies are the safest tier. linkedin sees thousands of users behind each carrier-grade nat ip, so individual scraping signals are diluted. expect to pay $50 to $150 per port per month.

    residential proxies are the value pick. session-rotating residential pools work for most scraping at $4 to $7 per gb. choose providers with sticky sessions of 10+ minutes so a single profile-view session does not change ip mid-flow.

    datacenter proxies are unusable for linkedin in 2026. even premium isp proxies (which are residential-issued datacenter ips) get blocked within a few requests.

    assign one proxy per linkedin account. never share an ip across multiple accounts. linkedin’s session correlation flags shared ips fast.

    account discipline

    aged accounts are non-negotiable in 2026. linkedin treats accounts under 6 months old with no activity as bots by default. for production scraping you need accounts with at least 100 connections, a complete profile, posted content from real timestamps, and a normal usage history.

    three options for account supply.

    option 1: warm your own. spend 30 to 60 days on each account: login, scroll, accept connections, post once a week, like a few posts daily. boring but the accounts last.

    option 2: buy aged accounts. resellers sell 1-year-old accounts with 500+ connections for $50 to $200. quality varies wildly. budget for replacement.

    option 3: managed scraping apis. let bright data, apify, or proxycurl handle the account problem entirely. you pay per query, they handle bans on their side. cleanest for production.

    never run more than 80 to 120 actions per account per day. one action = one profile view, one search, or one connection request. above that, ban risk spikes hard.

    browser automation: playwright with anti-detection

    for self-managed scraping, playwright with anti-detection patches is the baseline.

    from playwright.sync_api import sync_playwright
    import time
    import random
    
    PROXY = {
        "server": "http://proxy.example.com:8080",
        "username": "user-session-abc123",
        "password": "pass",
    }
    
    def scrape_profile(profile_url, session_cookie):
        with sync_playwright() as p:
            browser = p.chromium.launch(
                headless=True,
                proxy=PROXY,
                args=[
                    "--disable-blink-features=AutomationControlled",
                    "--no-sandbox",
                ],
            )
            context = browser.new_context(
                user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                           "AppleWebKit/537.36 (KHTML, like Gecko) "
                           "Chrome/127.0.0.0 Safari/537.36",
                viewport={"width": 1920, "height": 1080},
                locale="en-US",
                timezone_id="America/New_York",
            )
            context.add_cookies([{
                "name": "li_at",
                "value": session_cookie,
                "domain": ".linkedin.com",
                "path": "/",
            }])
    
            page = context.new_page()
            page.goto(profile_url, wait_until="networkidle")
            time.sleep(random.uniform(2, 5))
    
            page.mouse.wheel(0, 600)
            time.sleep(random.uniform(1, 3))
            page.mouse.wheel(0, 800)
            time.sleep(random.uniform(2, 4))
    
            name = page.locator("h1").inner_text()
            headline = page.locator(".text-body-medium.break-words").first.inner_text()
    
            browser.close()
            return {"name": name, "headline": headline, "url": profile_url}
    

    the --disable-blink-features=AutomationControlled flag removes the most obvious headless tell. sleeps and mouse-wheel events simulate human pacing. timezone, locale, and user-agent match a typical us desktop user.

    for stronger anti-detection, use playwright-stealth or a real antidetect browser like adspower or gologin. plain playwright is detectable by sophisticated fingerprinting.

    for the broader python scraping context see our web scraping with python guide.

    sticky sessions across the scrape session

    linkedin tracks ip across a session. switching ip mid-session looks like account hijacking and triggers a security challenge.

    def session_username(account_id):
        """build a sticky username for residential providers that support it."""
        return f"user-session-{account_id}"
    
    def proxy_for_account(account_id):
        return {
            "server": "http://proxy.example.com:8080",
            "username": session_username(account_id),
            "password": "pass",
        }
    

    most residential providers (smartproxy, oxylabs, soax) support session usernames that pin a single residential ip for 10 to 30 minutes. use the same session id for the duration of the linkedin scrape, then rotate when the session expires naturally.

    rate limits in practice

    based on 6 months of data across 30 aged accounts running through residential proxies in 2026, here is what stayed unbanned:

    • profile views: under 80 per day per account
    • searches: under 25 per day per account
    • connection requests: under 15 per day per account (lifetime cap of 100 per week)
    • messages to connections: under 50 per day per account
    • session length: 30 to 90 minutes per session, 1 to 2 sessions per day
    • gap between sessions: at least 4 hours

    push past these and ban rates spike. stay below them and accounts last 6 to 12 months on average before any restriction.

    managed scraping apis: the easier path

    self-managed linkedin scraping is a job. you maintain account warming, proxy rotation, anti-detection patches, and ban recovery. for many teams the time cost beats the api cost.

    managed options in 2026:

    bright data linkedin dataset. pre-scraped public profiles. updated continuously. you query by url or company. roughly $0.001 to $0.01 per record depending on volume. no scraping risk on your side.

    apify linkedin scraper actors. pay per actor run. simpler than building your own; still subject to linkedin’s anti-bot. 2026 prices: roughly $1 to $3 per 1,000 results.

    proxycurl. linkedin profile, company, and job api. enterprise-friendly with response-time slas. $0.10 to $0.30 per profile lookup at typical volumes.

    phantombuster. no-code linkedin automation. covers scraping plus connection requests and messaging. see our breakdown in outscraper vs phantombuster vs hunter.io.

    for production teams, the managed apis are the right choice unless you need volume that exceeds their rate limits or you are scraping data they do not offer.

    what to do when an account gets restricted

    linkedin restricts accounts in stages: warning, partial restriction (no search, no messages), full restriction (login redirects to verification), then permanent ban.

    at warning stage: stop all automation for 7 to 14 days. log in manually from a regular browser on the same proxy. do normal user activities (scroll, like 1 to 2 posts, accept 1 connection). most accounts recover.

    at partial restriction: same playbook plus complete identity verification if linkedin asks (selfie, government id). if you skip verification, the account moves to full restriction. for accounts you bought, this is usually game over.

    at full restriction: usually unrecoverable without verification. for managed-api stacks, this is on the api provider, not you.

    at permanent ban: replace the account. log the proxy + account combo so you do not reuse the proxy for the next account.

    ethical and security notes

    if you scrape eu data subjects, you must respect erasure requests. publish a privacy policy that lists linkedin as a data source and provides a removal email. process removals within 30 days.

    never scrape data behind a login that requires special permission (closed groups, private messages, premium-only fields). that crosses into the cfaa unauthorized-access territory in the us and is a clear gdpr violation in the eu.

    cold email or cold dm using scraped data still requires lawful basis in the eu and uk and a clear opt-out everywhere. a working email is a tool, not a license.

    faq

    can i scrape public linkedin profiles legally?

    in the us, public profile data scraping is generally legal under hiq v linkedin, but it violates linkedin’s tos. in the eu and uk, gdpr requires a lawful basis even for public data when it identifies a person. always document your basis and offer opt-out.

    what proxies should i use for linkedin scraping?

    mobile proxies are safest. residential proxies with sticky sessions of 10+ minutes are the value pick. datacenter and isp proxies are blocked instantly in 2026. budget $5 to $7 per gb for residential or $50+ per port per month for mobile.

    how many requests per day before linkedin bans?

    aged accounts on residential proxies tolerate roughly 80 profile views, 25 searches, and 15 connection requests per day. fresh accounts on datacenter ips tolerate maybe 20 to 50 requests before banning.

    is selenium or playwright better for linkedin scraping?

    playwright is the better default in 2026. its anti-detection options are richer, the api is cleaner, and it handles modern js rendering more reliably. selenium still works but requires more patches to avoid headless detection.

    do i need a paid linkedin sales nav account to scrape effectively?

    not strictly. public profile scraping works without a paid account. sales nav unlocks deeper search filters and lead lists, which is useful for outbound. paid accounts also tolerate slightly higher rate limits before triggering anti-bot.

    should i use a managed linkedin api or build my own scraper?

    for under 5,000 profiles per month, managed apis (bright data, apify, proxycurl) are usually cheaper than the engineering plus account management cost. for higher volume or unique fields not in the public datasets, build your own. budget for warming aged accounts, residential proxies, and ongoing maintenance.

    the bottom line

    linkedin scraping in 2026 is harder than 2022 because linkedin’s anti-bot stack got better. but it is also more accessible because managed datasets cover most common use cases at a per-record price that beats diy.

    self-managed approach: aged accounts, residential or mobile proxies (one per account), playwright with anti-detection, conservative rate limits. expect to replace 10 to 20 percent of accounts every quarter.

    managed approach: pay $0.001 to $0.30 per record depending on freshness and depth. zero ban exposure. faster time-to-data.

    for most teams in 2026 the managed approach wins on total cost. for teams scraping at very high volume or extracting fields managed apis do not surface, the diy stack still has a place. either way, document your gdpr basis and respect opt-outs. it is the difference between a sustainable lead-gen channel and a pile of legal exposure.

  • Outscraper vs PhantomBuster vs Hunter.io: B2B Lead Gen Tools Compared

    outscraper vs phantombuster vs hunter.io: b2b lead gen tools compared

    outscraper wins for google maps and review scraping at scale. phantombuster wins for linkedin automation and multi-platform workflows. hunter.io wins for email finding and verification. all three are mature in 2026 with established products and clear pricing. they solve different parts of the lead gen pipeline. most teams running b2b outbound use two of the three together rather than picking one.

    this comparison covers what each tool does well, where they overlap, what they cost, and how they fit into a 2026 outbound stack.

    quick verdict by job

    job best tool
    scrape google maps for local businesses outscraper
    pull google reviews and ratings outscraper
    extract emails from a domain hunter.io
    verify if an email actually delivers hunter.io
    find linkedin profiles by company phantombuster
    auto-message linkedin connections phantombuster
    scrape twitter, instagram, sales nav phantombuster
    build a list of agencies in a city outscraper
    find the cmo of a company hunter.io or phantombuster

    what each tool actually does

    outscraper

    outscraper sells public data scraping as a service. their api covers google maps places, google reviews, google search, ebay, amazon, yellow pages, trustpilot, and 30+ other sources.

    you submit a query (e.g. “dentists in singapore”) and outscraper returns a structured csv or json with name, address, phone, website, rating, review count, and contact details where available. for reviews you get the full review text plus author metadata.

    it is the most reliable way to build local business lists at scale without running your own scraping infrastructure.

    phantombuster

    phantombuster is a no-code scraping and automation platform with 100+ pre-built “phantoms” for linkedin, twitter, instagram, facebook, sales navigator, indeed, and others.

    each phantom is a worker that runs in the cloud on a schedule. you set up a linkedin search url, point a phantom at it, and it scrapes profiles into a csv. another phantom can take that csv and send connection requests with personalized messages.

    phantombuster excels at multi-step workflows. example: scrape sales nav search, enrich with hunter.io emails, send linkedin invites, then email follow-ups. the chaining is the moat.

    hunter.io

    hunter.io is the dominant email finder. you give it a company domain and it returns email addresses for people at that company, scored by confidence. their email verifier checks if a given email is deliverable without sending.

    the data comes from public web sources (company websites, mailing lists, github commits, signature scraping) and is updated continuously. they cover 200m+ companies.

    hunter is narrow but deep. emails are the only thing it does. they do it better than any general-purpose tool.

    pricing in 2026

    tier outscraper phantombuster hunter.io
    free 500 results/month free 14-day trial 25 lookups/month
    starter $35/mo (2,000 results) $69/mo (20 hours) $49/mo (500 lookups)
    business $99/mo (10,000 results) $159/mo (80 hours) $149/mo (2,500)
    enterprise custom $499/mo (300 hours) $499/mo (10,000)
    pay-per-use yes ($0.0035/result) no yes

    outscraper bills by results returned. phantombuster bills by execution time of phantoms. hunter bills by lookups (one email find = one lookup). these are different units, which makes direct comparison hard.

    for 1,000 google maps places per month, outscraper at $35 is cheapest. for the same data via phantombuster, you need a maps phantom (exists, slower) and budget around 5 hours of runtime, fitting in the $69 plan.

    for 500 emails per month, hunter at $49 is the price-per-email leader. phantombuster has an email finder phantom but it consumes execution time and is less accurate.

    data quality

    outscraper data quality is high for google maps fields (name, address, phone, hours, rating). emails and “owner contact” are best-effort enrichments and accuracy varies by region. for sg and us markets it works well; for sea outside major cities, expect 30 to 50 percent miss rates on emails.

    phantombuster data quality depends on the source. linkedin scrapes are reliable but limited by linkedin’s rate limits per session. instagram and twitter scrapes are less reliable because those platforms invest heavily in anti-bot. expect 60 to 90 percent accuracy depending on the phantom and recency of platform changes.

    hunter.io email accuracy is the highest in the category. their verifier reduces hard bounces to under 2 percent in our testing across 10,000 emails. for cold email deliverability, hunter is what your inbox-warming team will demand.

    gdpr and legal posture

    all three have public posture statements on gdpr and ccpa.

    outscraper scrapes only public data and provides standard privacy controls. they comply with subject access requests. their tos passes responsibility for downstream use to the buyer. read their outscraper terms before using for eu prospects.

    phantombuster sits in a grayer zone. linkedin scraping violates linkedin’s tos but is generally legal under us case law (hiq v linkedin). phantombuster’s tos disclaims liability for platform-tos violations and pushes that onto the user. for eu prospects, run gdpr legitimate-interest analysis and document it.

    hunter.io operates under legitimate interest in the eu and offers explicit subject removal flows. it is the cleanest of the three from a gdpr standpoint because it surfaces public-facing emails that companies have already published.

    for any cold outreach campaign in the eu or uk, document your lawful basis before sending. the european data protection board guidance is the source of record.

    integration and api

    feature outscraper phantombuster hunter.io
    rest api yes yes yes
    zapier yes yes yes
    make.com yes yes yes
    webhooks yes yes yes
    crm direct integrations hubspot, pipedrive hubspot, salesforce hubspot, salesforce, pipedrive
    chrome extension no yes (linkedin) yes

    phantombuster and hunter both ship chrome extensions. outscraper does not. for sales reps doing manual research alongside automation, the extensions matter. for engineers building a pipeline, the rest apis matter.

    all three offer good documentation and active 2026 maintenance. official refs: outscraper api, phantombuster api, hunter api.

    the typical b2b outbound stack in 2026

    most teams we see running outbound use two or three of these tools together.

    stack 1: local services targeting (agencies, dentists, contractors)

    • outscraper for the maps list
    • hunter.io for emails of decision-makers at each company
    • email outreach tool (mailchimp, instantly, smartlead)

    stack 2: linkedin-first b2b saas outbound

    • sales nav search url
    • phantombuster to scrape profiles + send connection requests
    • hunter.io to enrich profiles with emails for parallel email touch
    • crm to track responses

    stack 3: account-based marketing on enterprise targets

    • internal tam list
    • hunter.io to find decision-makers per company
    • phantombuster for linkedin warm-up
    • email outreach for direct touches

    few teams use only one tool. each fills a different stage of the funnel. for the proxy and ip side of any cold outbound see our proxies for lead generation guide.

    when to skip all three

    three scenarios where you do not need any of these.

    zoominfo or apollo subscriber. if you already pay for a contact-data platform, the email finder use case is covered. you may still want phantombuster for linkedin automation. for the proxy considerations on these platforms see our apollo and zoominfo proxies guide.

    referral-led growth. if 80 percent of your pipeline comes from referrals, scraped lists rarely outperform asking customers for warm intros.

    eu enterprise b2b. for large accounts in the eu, scraped data is risky. legitimate prospecting through events, content, and inbound has lower legal exposure.

    for the multi-account safety side of running phantombuster at scale see our multi-accounting proxies guide.

    cost example: 1000 quality leads per month

    target: 1000 verified b2b emails of saas marketing managers at companies with 50 to 500 employees.

    stack a: outscraper + hunter.io
    – outscraper: build seed list from linkedin company-id mapping + google maps presence: $35
    – hunter.io: 1000 lookups + 500 verifications: $49 to $99
    – total: roughly $85 to $135 per month

    stack b: phantombuster + hunter.io
    – phantombuster sales nav profile scraper: $69 (20 hours, fits 1000 profiles)
    – hunter.io: 1000 lookups: $49
    – total: $118 per month

    stack c: phantombuster only with email-finder phantom
    – phantombuster business plan: $159
    – email accuracy noticeably lower; expect to verify externally
    – total: $159, lower data quality

    practical advice: run hunter alongside whatever scraping tool you pick. the verification step is what keeps cold email deliverability above 95 percent.

    faq

    is outscraper legal to use?

    outscraper scrapes public data only. for eu and uk prospects, run a gdpr legitimate-interest analysis before using the data for outreach. for us prospects, ccpa applies. always offer an opt-out in your outreach.

    is phantombuster against linkedin’s tos?

    scraping linkedin technically violates their tos. us case law (hiq v linkedin) generally protects scraping of public profiles for now. linkedin actively rate-limits and bans suspected scraping accounts. use cooldowns, residential proxies (separate from your main account ip), and budget for occasional account replacements.

    what is the most accurate email finder in 2026?

    hunter.io is the leader for accuracy and deliverability in our testing across 10,000 emails. apollo and zoominfo can match it on enterprise targets but cost more. for budget tools, neverbounce and zerobounce are competitive verifiers but less strong on the discovery step.

    can these tools replace zoominfo or apollo?

    partially. for smb and local-business prospects, outscraper plus hunter.io covers most use cases at one-tenth the price. for enterprise contact data with org charts and intent signals, zoominfo and apollo still have richer datasets.

    do i need proxies to use phantombuster?

    phantombuster runs in their cloud with their own ip pool. you can also bring your own proxies to reduce risk on linkedin and other rate-limited targets. for paid linkedin sales nav accounts, residential proxies are recommended to avoid suspension.

    what is the cheapest way to build a b2b lead list in 2026?

    outscraper for the seed list (e.g. all dentists in a city) plus hunter.io for emails. for a 1,000-email list this runs around $85 per month total. cheaper than zoominfo, apollo, or building a custom scraper.

    the bottom line

    these tools solve different parts of the same funnel. outscraper handles structured data scraping at scale. phantombuster handles social platform automation. hunter.io handles email discovery and verification. the right answer is rarely “pick one” but “pick the two that fit your stack.”

    for local services and smb outbound, outscraper plus hunter is the cleanest combination. for linkedin-first b2b saas, phantombuster plus hunter is standard. for enterprise abm, layer all three onto a zoominfo or apollo seat.

    run gdpr lawful basis docs for eu prospects regardless of which tool you pick. cheap leads with bounces and complaints cost more than no leads at all.

  • How to Use Proxies with Scrapy: Middleware, Rotation, and Headers (2026)

    how to use proxies with scrapy: middleware, rotation, and headers (2026)

    scrapy supports proxies three ways: per-request meta, the built-in httpproxymiddleware, and custom rotating middleware. for a single proxy, set request.meta["proxy"]. for rotation, write a downloader middleware that picks a fresh proxy per request and tracks dead ones. for production, pair the rotating middleware with header spoofing and a retry policy. this tutorial gives you working code for all three patterns plus the gotchas that bite at scale.

    we cover the basics, then build a production-ready rotating middleware with health checks and exponential backoff.

    the simplest pattern: per-request proxy

    set proxy in request.meta. scrapy’s built-in httpproxymiddleware (enabled by default) reads it.

    import scrapy
    
    class SimpleSpider(scrapy.Spider):
        name = "simple"
        start_urls = ["https://httpbin.org/ip"]
    
        def start_requests(self):
            for url in self.start_urls:
                yield scrapy.Request(
                    url,
                    meta={"proxy": "http://user:pass@1.2.3.4:8080"},
                )
    
        def parse(self, response):
            self.logger.info(f"saw ip: {response.json()}")
    

    this is the right pattern for jobs with one or two static proxies. for rotation, build a middleware.

    env-based proxy via http_proxy

    if you want every request to go through one proxy without touching code, scrapy honors the http_proxy and https_proxy env vars:

    export HTTP_PROXY="http://user:pass@1.2.3.4:8080"
    export HTTPS_PROXY="http://user:pass@1.2.3.4:8080"
    scrapy crawl simple
    

    this works for ci pipelines and one-off runs. for fine-grained control, use the middleware approach below.

    rotating proxy middleware

    create myproject/middlewares.py:

    import random
    import time
    import logging
    from collections import defaultdict
    from scrapy import signals
    
    logger = logging.getLogger(__name__)
    
    
    class RotatingProxyMiddleware:
        """rotating proxy with health tracking and exponential cooldown."""
    
        def __init__(self, proxies, cooldown_sec=300):
            self.proxies = list(proxies)
            self.cooldown_sec = cooldown_sec
            self.bad_until = defaultdict(float)
            self.fail_count = defaultdict(int)
            if not self.proxies:
                raise ValueError("rotating proxy middleware: no proxies configured")
    
        @classmethod
        def from_crawler(cls, crawler):
            proxies = crawler.settings.getlist("ROTATING_PROXIES")
            cooldown = crawler.settings.getint("ROTATING_PROXY_COOLDOWN_SEC", 300)
            return cls(proxies=proxies, cooldown_sec=cooldown)
    
        def get_proxy(self):
            now = time.time()
            live = [p for p in self.proxies if self.bad_until[p] < now]
            if not live:
                logger.warning("all proxies cooling down. resetting.")
                self.bad_until.clear()
                live = self.proxies
            return random.choice(live)
    
        def mark_bad(self, proxy):
            self.fail_count[proxy] += 1
            cooldown = self.cooldown_sec * (5 ** (self.fail_count[proxy] - 1))
            self.bad_until[proxy] = time.time() + cooldown
            logger.info(f"proxy {proxy} marked bad. cooldown {cooldown}s.")
    
        def mark_good(self, proxy):
            self.fail_count[proxy] = 0
    
        def process_request(self, request, spider):
            if "proxy" in request.meta and request.meta.get("_proxy_assigned"):
                return
            proxy = self.get_proxy()
            request.meta["proxy"] = proxy
            request.meta["_proxy_assigned"] = True
    
        def process_response(self, request, response, spider):
            proxy = request.meta.get("proxy")
            if not proxy:
                return response
            if response.status in (407, 502, 503, 504):
                self.mark_bad(proxy)
            elif 200 <= response.status < 400:
                self.mark_good(proxy)
            return response
    
        def process_exception(self, request, exception, spider):
            proxy = request.meta.get("proxy")
            if proxy:
                self.mark_bad(proxy)
    

    enable in settings.py:

    DOWNLOADER_MIDDLEWARES = {
        "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 750,
        "myproject.middlewares.RotatingProxyMiddleware": 760,
    }
    
    ROTATING_PROXIES = [
        "http://user:pass@1.2.3.4:8080",
        "http://user:pass@5.6.7.8:8080",
        "http://user:pass@9.10.11.12:8080",
    ]
    
    ROTATING_PROXY_COOLDOWN_SEC = 300
    

    the middleware picks a fresh proxy per request, marks dead proxies on 407/502/503/504 responses or exceptions, and applies exponential cooldown so a flaky proxy comes back online after a short rest.

    sticky session middleware for login flows

    some scrapes need the same proxy across multiple requests (login then crawl). hash the session id to a fixed proxy:

    import hashlib
    
    class StickyProxyMiddleware:
        def __init__(self, proxies):
            self.proxies = list(proxies)
    
        @classmethod
        def from_crawler(cls, crawler):
            return cls(crawler.settings.getlist("STICKY_PROXIES"))
    
        def process_request(self, request, spider):
            session_id = request.meta.get("session_id")
            if not session_id:
                return
            h = hashlib.md5(session_id.encode()).hexdigest()
            idx = int(h, 16) % len(self.proxies)
            request.meta["proxy"] = self.proxies[idx]
    

    usage in spider:

    yield scrapy.Request(
        "https://example.com/dashboard",
        meta={"session_id": "user_abc"},
        callback=self.parse_dashboard,
    )
    

    every request with session_id="user_abc" gets the same proxy. swap to a different session id and you get a different proxy.

    for the deeper architecture pattern across multiple workers, see our proxy load balancing architecture guide.

    header spoofing alongside proxies

    a fresh ip with stale headers fingerprints obviously. pair the rotating middleware with rotating user agents and accept-language headers:

    class RotatingHeadersMiddleware:
        USER_AGENTS = [
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
        ]
    
        def process_request(self, request, spider):
            request.headers["User-Agent"] = random.choice(self.USER_AGENTS)
            request.headers["Accept-Language"] = "en-US,en;q=0.9"
            request.headers["Accept-Encoding"] = "gzip, deflate, br"
    

    enable below the proxy middleware in settings.py:

    DOWNLOADER_MIDDLEWARES = {
        "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 750,
        "myproject.middlewares.RotatingProxyMiddleware": 760,
        "myproject.middlewares.RotatingHeadersMiddleware": 770,
    }
    

    for finer fingerprint control (tls, http2, browser headers), use a managed scraping api or a headless browser. plain http requests cannot fully spoof a chrome client.

    scrapy retry settings

    scrapy ships with a retry middleware. configure it to match the rotating proxy logic:

    RETRY_ENABLED = True
    RETRY_TIMES = 3
    RETRY_HTTP_CODES = [403, 408, 429, 500, 502, 503, 504]
    
    DOWNLOAD_TIMEOUT = 15
    CONCURRENT_REQUESTS = 32
    CONCURRENT_REQUESTS_PER_DOMAIN = 8
    
    DOWNLOAD_DELAY = 0.5
    RANDOMIZE_DOWNLOAD_DELAY = True
    

    RETRY_HTTP_CODES = [403, 408, 429, 500, 502, 503, 504] retries common rate-limit and proxy-failure responses. combined with the rotating middleware, each retry picks a fresh proxy.

    CONCURRENT_REQUESTS_PER_DOMAIN = 8 is conservative. tune up for tolerant targets, down for strict ones. the rotating middleware does not rate-limit; that is the autothrottle’s job.

    autothrottle for rate-limit safety

    AUTOTHROTTLE_ENABLED = True
    AUTOTHROTTLE_START_DELAY = 1.0
    AUTOTHROTTLE_MAX_DELAY = 60.0
    AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0
    AUTOTHROTTLE_DEBUG = False
    

    autothrottle backs off when the target slows down or returns errors. with rotating proxies, this prevents a target from blocking your full pool by detecting a burst.

    handling 407 proxy auth required

    if you see 407 proxy authentication required errors, three checks:

    1. proxy url format is http://user:pass@host:port exactly. no leading whitespace, no url-encoded user.
    2. some providers require username sessions (user-session-abc123). use the full session-username from your dashboard.
    3. scrapy’s httpproxymiddleware does not always pass the basic-auth header automatically for some legacy versions. if you hit this, add proxy-authorization explicitly:
    from base64 import b64encode
    
    class ProxyAuthMiddleware:
        def process_request(self, request, spider):
            proxy = request.meta.get("proxy")
            if not proxy or "@" not in proxy:
                return
            creds = proxy.split("//", 1)[1].split("@", 1)[0]
            token = b64encode(creds.encode()).decode()
            request.headers["Proxy-Authorization"] = f"Basic {token}"
    

    scrapy 2.11+ handles this automatically. older versions need this snippet.

    benchmark: 10,000 pages with rotating proxies

    across 10,000 pages of a tolerant ecommerce target, with a 50-proxy residential pool, the configuration above completed in roughly 22 minutes on a single mac workstation. that is around 7.5 requests per second sustained.

    failed requests (mostly 503s) hit 4 percent. retries succeeded 92 percent of the time. proxies marked bad: 11 of 50 over the run. all 11 came back online within an hour as cooldown expired.

    scaling to 100,000 pages, the same config runs in 3 to 4 hours. for higher throughput, run multiple scrapy processes against the same proxy pool with a shared bad-proxy state stored in redis.

    production checklist

    four items separate hobby spiders from production scrapy deployments.

    shared bad-proxy state. for multi-worker setups, store the bad-proxy list in redis instead of in-process memory. otherwise each worker re-discovers the same dead proxies independently.

    per-domain proxy pools. for sites that ban entire ranges, segment your proxy pool by target domain. keep a clean residential pool for hard targets and reuse a cheaper datacenter pool for tolerant ones.

    playwright integration. for js-heavy targets, use scrapy-playwright. it integrates with the rotating middleware via request.meta["playwright_context_kwargs"]["proxy"].

    logging. log every request with proxy, status, latency, and final response code. for postmortems on broken scrapes, this is what you analyze.

    for the broader python scraping context see our web scraping with python guide.

    faq

    what is the easiest way to add a proxy in scrapy?

    set request.meta["proxy"] = "http://user:pass@host:port" per request. scrapy’s built-in httpproxymiddleware handles the rest. enabled by default.

    does scrapy support proxy rotation out of the box?

    no. scrapy’s httpproxymiddleware uses one proxy per request based on request.meta. for rotation across requests, write a downloader middleware (full code in this tutorial) or install scrapy-rotating-proxies from pypi.

    how do i use socks5 proxies with scrapy?

    scrapy supports socks5 via twisted. use socks5://user:pass@host:port in request.meta["proxy"]. older scrapy versions need pip install txsocksx for full socks5 support.

    why am i getting 407 errors with scrapy proxies?

    usually wrong credentials format. confirm http://user:pass@host:port exactly. for residential providers using session-id auth, paste the full session-username (e.g. user-session-abc123) in the user field.

    should i use scrapy-rotating-proxies or write my own middleware?

    scrapy-rotating-proxies is fine for simple rotation. for production with custom health checks, sticky sessions, or per-domain pools, write your own. the middleware in this tutorial is around 50 lines and gives full control.

    how do i debug scrapy proxy issues?

    run with -L DEBUG to see every request and proxy assignment. log the response status and request.meta["proxy"] in your spider’s parse methods. for tls or auth issues, run the same proxy against curl -x first to isolate scrapy from the proxy itself. official docs at the scrapy reference.

    the bottom line

    scrapy’s proxy story is built on three pieces: per-request meta, the built-in httpproxymiddleware, and your custom rotating middleware. with the middleware in this tutorial plus header rotation and autothrottle, you have a production-grade scraper that survives dead proxies, rate limits, and the long tail of target-specific failures.

    for jobs above 100,000 pages or with strict anti-bot, pair this stack with residential proxies and a shared redis bad-proxy state. for lighter jobs, the in-process version above is enough.

    start with the per-request pattern, add the rotating middleware once you have more than 5 proxies, and add sticky sessions when you hit your first login flow. each layer composes cleanly with scrapy’s existing machinery.

  • ScrapeGraphAI Tutorial: AI-Powered Scraping Without Selectors (2026)

    scrapegraphai tutorial: ai-powered scraping without selectors (2026)

    scrapegraphai is an open-source python library that scrapes any website by describing what you want in plain english. it sends the rendered html to an llm, which extracts structured json without you writing css or xpath selectors. install with pip install scrapegraphai, plug in an openai or local ollama key, point it at a url, and it returns parsed data. it is useful for one-off scrapes, prototype work, and small sites where selectors break weekly.

    this tutorial covers install, the four pipeline types, proxy and headless integration, and when to use it versus a traditional scrapy or playwright stack.

    what scrapegraphai is

    scrapegraphai (github: scrapegraph-ai/scrapegraph-ai) is a graph-based web scraping framework that uses an llm to extract data instead of selectors.

    the workflow:

    1. you provide a url and a natural-language prompt (“get all product names and prices”).
    2. scrapegraphai fetches the page (with optional headless browser).
    3. it cleans and chunks the html.
    4. an llm parses the chunks into structured json matching your prompt.

    no css, xpath, or regex. selector drift on the target site does not break your scraper unless the page structure changes so much the llm cannot find the data.

    installation in 2026

    pip install scrapegraphai
    playwright install chromium
    

    the playwright install is needed for the smart_scraper graph that uses a real browser. for static-html scraping (no js), the playwright step is optional.

    set your llm api key as an environment variable:

    export OPENAI_API_KEY="sk-..."
    

    scrapegraphai supports openai, anthropic, groq, and local ollama out of the box. for cost-conscious development, use ollama with a 7b model.

    your first scrape: smartscraper graph

    from scrapegraphai.graphs import SmartScraperGraph
    
    graph_config = {
        "llm": {
            "api_key": "sk-...",
            "model": "openai/gpt-4o-mini",
        },
        "verbose": False,
        "headless": True,
    }
    
    scraper = SmartScraperGraph(
        prompt="list all article titles and their authors on this page",
        source="https://hnrss.org/frontpage",
        config=graph_config,
    )
    
    result = scraper.run()
    print(result)
    

    output (truncated):

    {
        "articles": [
            {"title": "show hn: a new approach to ai scraping", "author": "alex"},
            {"title": "rust 1.78 released", "author": "rust-lang team"},
            ...
        ]
    }
    

    no selectors. the llm read the rendered page and returned what you asked for in json.

    the four main graph types

    smartscrapergraph

    single-page extraction. you give it a url and a prompt, it returns json. this is the workhorse for 80 percent of use cases.

    searchgraph

    google-search-driven scraping. you give it a query, it searches google, picks top results, and runs smartscraper on each.

    from scrapegraphai.graphs import SearchGraph
    
    graph = SearchGraph(
        prompt="find python scraping libraries with examples",
        config=graph_config,
    )
    result = graph.run()
    

    useful for research-style scraping where you do not have a fixed url list.

    speechgraph

    extracts data and converts the result to audio via tts. useful for accessibility apps. less commonly used but it ships in the library.

    smartscrapermultigraph

    batch version of smartscraper. give it a list of urls, run the same prompt against each in parallel.

    from scrapegraphai.graphs import SmartScraperMultiGraph
    
    urls = [
        "https://example.com/product/1",
        "https://example.com/product/2",
        "https://example.com/product/3",
    ]
    
    graph = SmartScraperMultiGraph(
        prompt="extract product name, price, and stock status",
        source=urls,
        config=graph_config,
    )
    result = graph.run()
    

    the multi-graph is concurrent under the hood. it is the right pick for scraping a list of similar pages.

    adding proxies

    production scrapers need proxies. scrapegraphai accepts a proxy in the config:

    graph_config = {
        "llm": {
            "api_key": "sk-...",
            "model": "openai/gpt-4o-mini",
        },
        "loader_kwargs": {
            "proxy": {
                "server": "http://proxy.example.com:8080",
                "username": "user",
                "password": "pass",
            },
        },
        "headless": True,
    }
    

    this passes the proxy to playwright, which routes both the page fetch and any sub-resources through the proxy.

    for proxy rotation across many requests, wrap your scrape calls in a loop and switch the config per call. for the full pattern see our python proxy rotation guide.

    using local llms with ollama

    api costs add up fast on real workloads. each smartscraper run sends the rendered html to the llm, which can be 5,000 to 50,000 tokens. for high-volume scraping, run a local model.

    ollama pull llama3.1:8b
    ollama serve
    

    then update config:

    graph_config = {
        "llm": {
            "model": "ollama/llama3.1",
            "temperature": 0,
            "format": "json",
            "model_tokens": 8192,
            "base_url": "http://localhost:11434",
        },
        "embeddings": {
            "model": "ollama/nomic-embed-text",
            "base_url": "http://localhost:11434",
        },
        "verbose": False,
    }
    

    a 4060ti or m2 max can run llama3.1 8b at usable speed for scraping. the trade-off is extraction quality. gpt-4o-mini is more reliable on messy pages than 8b local models.

    for cost-free development and prototyping, ollama is the right choice. for production, gpt-4o-mini at $0.15 per million input tokens is usually cheaper than running a gpu.

    handling js-heavy and login-walled sites

    smartscraper uses playwright by default with headless: true. for sites that require login, pass cookies via playwright before scraping:

    from scrapegraphai.graphs import SmartScraperGraph
    
    graph_config = {
        "llm": {"api_key": "sk-...", "model": "openai/gpt-4o-mini"},
        "loader_kwargs": {
            "extra_http_headers": {
                "cookie": "session=abc123; user_token=xyz",
            },
        },
        "headless": True,
    }
    

    for sites with strong anti-bot (cloudflare, datadome, perimeterx), pair scrapegraphai with a residential proxy. the llm can still parse the rendered page, but you need a browser the target lets through. see our best web scraping apis comparison for managed options that bundle this.

    when to use scrapegraphai vs traditional scrapy

    scenario use scrapegraphai use scrapy/playwright
    one-off research scrape yes overkill
    10 to 100 pages, low frequency yes works either way
    10,000+ pages per day maybe (cost-sensitive) yes
    schema is stable and well-known overkill yes
    schema changes weekly yes painful with selectors
    target site uses heavy js yes yes (with playwright)
    budget under $5 per scrape job scrapegraphai with ollama yes
    budget under $0.50 per scrape job yes (gpt-4o-mini) yes

    for high-volume production scraping with a stable schema, a hand-coded scrapy spider is still cheaper and more reliable. for quick scrapes, prototypes, or sites where the html structure shifts, scrapegraphai saves significant time.

    cost math for openai api

    gpt-4o-mini in 2026 is roughly $0.15 per million input tokens and $0.60 per million output tokens.

    a typical product-page scrape sends 10,000 input tokens (cleaned html) and outputs 500 tokens (json). cost per page:

    • input: 10,000 / 1,000,000 * $0.15 = $0.0015
    • output: 500 / 1,000,000 * $0.60 = $0.0003
    • total: roughly $0.0018 per page

    for 1000 pages, $1.80. for 100,000 pages, $180. budget llm cost into your scrape estimate.

    debugging tips

    set verbose=True to see every llm call and intermediate output. this is the fastest way to figure out why a prompt is not extracting what you expect.

    start prompts simple. “list all product names” works better than a 5-clause instruction with edge cases. add complexity once the basic prompt works.

    inspect the cleaned html scrapegraphai sends to the llm. it strips scripts, styles, and a lot of noise. if your target data is in a script tag or rendered late, you may need to pre-render harder before passing to the graph.

    for stable schemas, define a pydantic model and pass it as the schema arg. the llm will fill the model exactly, which improves consistency.

    from pydantic import BaseModel
    from typing import List
    
    class Product(BaseModel):
        name: str
        price: float
        in_stock: bool
    
    class ProductList(BaseModel):
        products: List[Product]
    
    scraper = SmartScraperGraph(
        prompt="extract all products",
        source="https://example.com/shop",
        config=graph_config,
        schema=ProductList,
    )
    

    official docs at the scrapegraphai github.

    faq

    what is scrapegraphai used for?

    ai-powered web scraping. you describe what you want in english and an llm extracts structured json from any url, no css or xpath needed.

    is scrapegraphai free?

    yes, the library is open source. you pay only for the llm api you use (openai, anthropic, etc.). with ollama and a local model, the entire stack is free.

    does scrapegraphai handle javascript-rendered pages?

    yes. it uses playwright under the hood with headless: true by default. for sites that need to scroll or click before content loads, you can extend the loader to run custom js.

    how does scrapegraphai compare to firecrawl?

    firecrawl is a managed scraping api. scrapegraphai is a self-hosted python library. firecrawl handles the infra and proxies. scrapegraphai gives you full control and lower cost at scale, but you wire up your own browser and proxies.

    can i use scrapegraphai with proxies?

    yes. pass proxy details in loader_kwargs.proxy. it routes through playwright. for rotation across many requests, swap the proxy per call or wrap in a custom session pool.

    what is the cost per page using scrapegraphai with gpt-4o-mini?

    roughly $0.0018 per page for a typical product page (10k input tokens, 500 output tokens). 1000 pages costs about $1.80. for high-volume production, run ollama locally to drop llm cost to zero.

    the bottom line

    scrapegraphai is the right tool for prototype scrapes, schema-flexible jobs, and sites where selectors break too often to maintain. with gpt-4o-mini, the cost is around $0.002 per page, which beats most managed scraping apis at small scale.

    for high-volume production with a stable target, scrapy with proper selectors is still cheaper and faster. but for the long tail of “i need to scrape this once and i do not want to write selectors,” scrapegraphai is the fastest path from url to json in 2026.

    start with smartscrapergraph, add proxies once you scale, and switch to ollama if api costs become the bottleneck. the library is actively developed and the api is stable enough to depend on.

  • Proxy Rotation with Python: aiohttp, httpx, and requests Compared (2026)

    proxy rotation with python: aiohttp, httpx, and requests compared (2026)

    proxy rotation in python boils down to picking a proxy per request, retrying on failure, and tracking which proxies still work. requests is the simplest, httpx is the modern sync+async pick, and aiohttp is the fastest at scale. across 1000 requests against a residential pool, aiohttp finished in 14 seconds, httpx in 19 seconds (async mode), and requests in 142 seconds (single-thread). pick the library based on concurrency needs, not the rotation logic itself.

    this tutorial gives you working code for all three, plus retry, sticky sessions, and a benchmark you can run yourself.

    the basic rotation pattern

    every proxy rotation script follows the same shape:

    1. load proxy list (file, env, or api).
    2. on each request, pick the next proxy (round-robin or random).
    3. catch errors. on failure, mark the proxy bad and retry with another.
    4. for sticky sessions, hash the target url or session-id to a fixed proxy.

    we will implement this in three libraries.

    requests: simplest, blocking

    requests is the right choice when you have under 50 requests per minute, no async constraints, and want minimal dependencies.

    import requests
    import random
    import time
    from itertools import cycle
    
    PROXIES = [
        "http://user:pass@1.2.3.4:8080",
        "http://user:pass@5.6.7.8:8080",
        "http://user:pass@9.10.11.12:8080",
    ]
    
    def rotate_get(url, max_retries=3, timeout=10):
        proxies_iter = cycle(random.sample(PROXIES, len(PROXIES)))
        last_err = None
    
        for _ in range(max_retries):
            proxy = next(proxies_iter)
            try:
                r = requests.get(
                    url,
                    proxies={"http": proxy, "https": proxy},
                    timeout=timeout,
                )
                r.raise_for_status()
                return r
            except Exception as e:
                last_err = e
                time.sleep(0.5)
    
        raise last_err
    
    resp = rotate_get("https://httpbin.org/ip")
    print(resp.json())
    

    this gives you round-robin rotation with 3-retry fallback. at 142 seconds for 1000 requests, it works for low-volume jobs.

    httpx: modern, sync or async

    httpx supports the same api as requests but adds full async support and http/2. for new code in 2026, prefer httpx over requests.

    import httpx
    import asyncio
    import random
    
    PROXIES = [
        "http://user:pass@1.2.3.4:8080",
        "http://user:pass@5.6.7.8:8080",
    ]
    
    async def fetch(url, max_retries=3):
        for _ in range(max_retries):
            proxy = random.choice(PROXIES)
            try:
                async with httpx.AsyncClient(
                    proxy=proxy,
                    timeout=10,
                    http2=True,
                ) as client:
                    r = await client.get(url)
                    r.raise_for_status()
                    return r.json()
            except Exception:
                await asyncio.sleep(0.3)
        raise RuntimeError("all retries failed")
    
    async def main():
        urls = [f"https://httpbin.org/anything?i={i}" for i in range(50)]
        results = await asyncio.gather(*[fetch(u) for u in urls])
        print(f"fetched {len(results)} urls")
    
    asyncio.run(main())
    

    httpx in async mode finished our 1000-request benchmark in 19 seconds. for sync mode, swap httpx.AsyncClient for httpx.Client and drop await.

    aiohttp: fastest at scale

    aiohttp is the highest-throughput async library in python. for any job above 100 requests per second, it beats httpx in our benchmarks.

    import aiohttp
    import asyncio
    import random
    
    PROXIES = [
        "http://user:pass@1.2.3.4:8080",
        "http://user:pass@5.6.7.8:8080",
    ]
    
    async def fetch(session, url, max_retries=3):
        for _ in range(max_retries):
            proxy = random.choice(PROXIES)
            try:
                async with session.get(
                    url,
                    proxy=proxy,
                    timeout=aiohttp.ClientTimeout(total=10),
                ) as r:
                    r.raise_for_status()
                    return await r.json()
            except Exception:
                await asyncio.sleep(0.3)
        raise RuntimeError("all retries failed")
    
    async def main():
        connector = aiohttp.TCPConnector(limit=100)
        async with aiohttp.ClientSession(connector=connector) as session:
            urls = [f"https://httpbin.org/anything?i={i}" for i in range(1000)]
            results = await asyncio.gather(*[fetch(session, u) for u in urls])
            print(f"fetched {len(results)} urls")
    
    asyncio.run(main())
    

    aiohttp finished 1000 requests in 14 seconds in our test. the TCPConnector(limit=100) controls max concurrent connections; tune this based on your proxy pool size and target site rate limits.

    for the scrapy ecosystem variant see our scrapy proxy middleware tutorial.

    proxy health tracking

    production scrapers need to drop dead proxies, not retry them forever. add a health-tracker:

    import time
    from collections import defaultdict
    
    class ProxyPool:
        def __init__(self, proxies, cooldown_sec=300):
            self.proxies = list(proxies)
            self.cooldown_sec = cooldown_sec
            self.bad_until = defaultdict(float)
            self.fail_count = defaultdict(int)
    
        def get(self):
            now = time.time()
            live = [p for p in self.proxies if self.bad_until[p] < now]
            if not live:
                # everything cooling down. reset and try again
                self.bad_until.clear()
                live = self.proxies
            return random.choice(live)
    
        def mark_bad(self, proxy):
            self.fail_count[proxy] += 1
            # exponential cooldown: 5 min, 25 min, 125 min...
            cooldown = self.cooldown_sec * (5 ** (self.fail_count[proxy] - 1))
            self.bad_until[proxy] = time.time() + cooldown
    
        def mark_good(self, proxy):
            self.fail_count[proxy] = 0
            self.bad_until[proxy] = 0
    

    drop this into any of the rotation patterns above. on success call pool.mark_good(proxy); on failure call pool.mark_bad(proxy).

    for residential pools that rotate the underlying ip on every request, proxy health is less of an issue. for static datacenter pools, this pattern is critical.

    sticky sessions

    some scraping targets break if you switch ip mid-session (login flows, multi-page checkout, captcha challenges). pin the proxy to a session id:

    import hashlib
    
    def sticky_proxy(session_id, proxies):
        h = hashlib.md5(session_id.encode()).hexdigest()
        idx = int(h, 16) % len(proxies)
        return proxies[idx]
    
    # same session_id always gets same proxy
    proxy = sticky_proxy("user_abc_session_123", PROXIES)
    

    for residential providers that natively support sticky sessions (smartproxy, oxylabs, soax), pass the session-id inside the username field instead:

    proxy = f"http://user-session-{session_id}:pass@proxy.example.com:7777"
    

    this leans on the provider to keep the session pinned for 1 to 30 minutes (varies by provider). it is cleaner than building your own sticky logic.

    for the proxy types that pair best with rotation see rotating proxies with unlimited bandwidth.

    benchmark: 1000 requests against httpbin.org/anything

    we ran each library against https://httpbin.org/anything 1000 times through a residential pool of 50 proxies, on a 4-core mac, with 100 concurrent connections.

    library mode time requests/sec
    requests sync, single-thread 142s 7
    requests sync, threadpool 50 18s 56
    httpx async 19s 53
    aiohttp async 14s 71

    for blocking single-threaded code, aiohttp is 10x faster than requests. with a threadpool wrapping requests, the gap closes to 1.3x. for new code, async is the right choice; the difference is library polish.

    error handling cheatsheet

    error usual cause fix
    ProxyError, ConnectionRefusedError proxy is dead mark bad, rotate
    ReadTimeout proxy is slow or target is slow retry with longer timeout
    407 Proxy Authentication Required wrong user/pass check credentials
    403 Forbidden from target ip flagged rotate to fresh ip
    429 Too Many Requests from target hit target rate limit back off, slower rotation
    SSL: WRONG_VERSION_NUMBER http proxy with https:// scheme use http:// for proxy url, even for https targets

    the last one bites everyone once. the proxy url scheme refers to the proxy protocol, not the target. for an http proxy use http://user:pass@ip:port regardless of whether the target is http or https.

    production patterns

    three patterns separate hobby scrapers from production.

    queue-driven workers. instead of looping through urls in-line, push them to a redis queue and run aiohttp workers that pop, fetch, and push results. survives crashes and scales horizontally.

    per-target rate limits. one global concurrency limit is wrong. add per-domain semaphores so a slow target does not starve a fast one.

    observability. log every request with proxy, status, latency. when scraping breaks, you need to know if proxies are dying or if the target changed.

    for the full python scraping stack see our web scraping with python guide.

    faq

    which python library is fastest for proxy rotation in 2026?

    aiohttp leads in our benchmark at 71 requests per second, followed by httpx async at 53 and requests with threadpool at 56. for new code, both aiohttp and httpx are good picks. requests still works for low-volume jobs.

    do i need a rotating proxy provider or can i build rotation myself?

    if you have a static list of proxies, build rotation in your code. if you want auto-rotation on every request from a residential pool, providers like smartproxy, oxylabs, and bright data handle it server-side. either approach works; the choice is operational, not technical.

    how often should i rotate proxies?

    every request for one-shot scrapes, every 1 to 30 minutes for session-based flows. for login or checkout flows, pin the proxy for the duration of the session.

    how do i detect a dead proxy?

    connection errors, 407 auth errors, and timeouts longer than 10 seconds. use exponential cooldowns (5 min first, 25 min second, 125 min third) so a transient blip does not permanently kill a good proxy.

    should i use http or socks5 proxies for python scraping?

    http is fine for most scraping (https included). socks5 only matters when you need to tunnel non-http traffic or when the proxy is socks5-only. requests, httpx, and aiohttp all support socks5 via pip install httpx[socks] or the aiohttp-socks extension.

    where do i find documentation for these libraries?

    official docs: requests, httpx, aiohttp. all three are actively maintained in 2026.

    the bottom line

    proxy rotation in python is 30 lines of code plus a health tracker. the library choice matters less than getting retry, cooldown, and sticky-session logic right.

    for jobs under 100 requests per minute, requests with a threadpool is the simplest. for everything else, aiohttp gives the best throughput. httpx sits in the middle with a friendlier api and full async support.

    start with the patterns above, add health tracking when you hit your first dead-proxy incident, and add per-domain rate limiting when you scrape multiple targets in parallel. the rest is operational discipline, not code.

  • Best Data Marketplace Platforms 2026: Where to Buy and Sell Datasets

    best data marketplace platforms 2026: where to buy and sell datasets

    the top 10 data marketplaces in 2026 are aws data exchange, snowflake marketplace, databricks marketplace, dawex, datarade, narrative.io, bright data dataset marketplace, kaggle datasets, datafiniti, and data.world. aws and snowflake dominate enterprise. dawex and datarade are the leading neutral platforms. kaggle stays the largest free pool for analysts. each handles delivery, payouts, and licensing differently.

    this guide breaks down what each platform sells, how sellers get paid, and which marketplace fits buyer use cases from b2b leads to alt-data hedge fund inputs.

    quick comparison: 2026 data marketplaces

    marketplace best for pricing model seller payout dataset count
    aws data exchange aws-native enterprise subscription, one-time 70/30 3,500+
    snowflake marketplace snowflake customers revenue share, subscription 90/10 2,000+
    databricks marketplace lakehouse users free or revenue share 90/10 1,000+
    dawex neutral b2b data one-time or subscription 80/20 600+
    datarade discovery-first varies by seller seller-set 2,500+
    narrative.io identity and audiences subscription varies 200+
    bright data scraped public data subscription n/a (single seller) 200+
    kaggle analysts and ml mostly free n/a 300,000+
    datafiniti retail and business listings api credits 70/30 n/a
    data.world community and open data freemium varies 100,000+

    aws data exchange

    aws data exchange is the default for enterprise buyers running on aws. data is delivered as files into your s3, redshift, or via api. payment runs through your aws bill so procurement is simple.

    categories include financial data (refinitiv, factset), healthcare (iqvia), location (here, foursquare), and weather (the weather company). entry prices range from free to $50,000 per month for premium feeds.

    sellers get 70 percent of revenue. integration with aws billing and analytics workloads is the moat. if your buyer or your team is already on aws, this is where to start.

    snowflake marketplace

    snowflake marketplace delivers data as live shares directly into your snowflake account. there is no etl or file copy; the data appears as a database you can query immediately.

    this is the cleanest delivery model in the industry. for snowflake-native teams it is faster to integrate than any other marketplace by an order of magnitude.

    sellers earn 90 percent of revenue, the highest split among major platforms. dataset count is smaller than aws but quality skews higher because most listings are vetted. categories include market data, identity graphs, and consumer panels.

    databricks marketplace

    databricks marketplace launched in 2023 and grew fast in 2025-2026. data is delivered as delta sharing tables, similar to snowflake’s live share but cross-cloud.

    it currently leans heavily on free public datasets and ai training corpora. paid commercial listings exist but the catalog is smaller than snowflake. sellers earn 90 percent on paid datasets.

    if your team is on databricks lakehouse, this is the natural fit. otherwise the snowflake or aws marketplaces have deeper paid catalogs.

    dawex

    dawex is the leading neutral b2b data exchange. it is cloud-agnostic, sells one-time and recurring data products, and runs in multiple regions including europe (gdpr-friendly defaults) and asia.

    their seller dashboard is the most polished outside the hyperscalers. you upload, set licensing terms, price per buyer or per region, and dawex handles payments and contracts.

    revenue share is 80/20 in favor of sellers. for vertical-specific datasets (mobility, energy, retail) dawex often has more depth than aws or snowflake.

    datarade

    datarade is more of a discovery layer than a marketplace. they list 2,500+ data products from 500+ providers, route buyer rfqs, and broker the deal. the actual data delivery happens off-platform between buyer and seller.

    this works well for buyers who want one search across many providers. it is less smooth than snowflake’s in-account share. seller pricing is fully provider-set.

    datarade is the right starting point if you do not yet know which provider has what you need. their search filters by category, geography, and use case are the best in the industry.

    narrative.io

    narrative.io specializes in identity, audience, and consumer data. they run a real-time bidding-style data ops platform where data is licensed in continuous streams, not files.

    if you are buying audience segments for ad targeting, customer enrichment for crm, or identity graphs for fraud, narrative is purpose-built. for static datasets, look elsewhere.

    bright data dataset marketplace

    bright data sells pre-scraped public web data as datasets: linkedin profiles, amazon products, instagram, tiktok, glassdoor reviews, indeed jobs, and 200+ more. you buy the most recent snapshot or subscribe to refreshes.

    this is single-seller (bright data only) but the depth on public web data is unmatched. for competitive intelligence, e-commerce pricing, and social listening, the time-to-data is faster than scraping yourself.

    pricing is per record or per gb. for context on pricing benchmarks see our proxy pricing comparison and the best web scraping apis guide.

    kaggle datasets

    kaggle hosts 300,000+ free datasets used mainly by analysts, ml engineers, and competition participants. licensing varies; many are creative commons or public domain.

    it is not a commercial marketplace. there is no payment infra and no commercial licensing layer. for prototyping a model or learning, it is the largest free pool. for production data licensing, look at the platforms above.

    datafiniti

    datafiniti specializes in business listings, product data, and property records as api endpoints. you pay per query or per record. the data is scraped and normalized from public sources.

    it competes with bright data’s dataset marketplace and outscraper for similar use cases (lead enrichment, retail intelligence). for our take on the b2b lead tool category see outscraper vs phantombuster vs hunter.io.

    data.world

    data.world started as an open data community. in 2024 it pivoted to enterprise data catalog and governance, and the marketplace component is now secondary to their cataloging product.

    for free open data with social features (comments, queries, shared notebooks) it remains useful. for commercial licensing it is no longer competitive.

    buyer checklist before purchasing

    four checks save weeks of contract back-and-forth.

    licensing. confirm whether the data is licensed for internal use, commercial product use, or resyndication. these are three different price tiers on most platforms.

    freshness. ask the seller for the typical data delivery cadence (real-time, daily, weekly, monthly) and what “current” means in their schema. some “live” feeds are 24 hours stale.

    geography and pii. for eu and uk buyers, confirm gdpr basis. for california buyers, confirm ccpa compliance. data marketplaces are intermediaries, not regulators.

    trial access. every reputable marketplace offers a sample. always pull 1,000 rows before committing to a year-long contract. many “verified” datasets are not what the marketing claims.

    for news data buyers specifically see our news apis comparison which covers similar buying decisions for content feeds.

    seller checklist before listing

    want to monetize data you already collect? four things matter.

    audience. listing on snowflake reaches snowflake customers. listing on aws reaches aws customers. listing on dawex reaches everyone but with less integration. pick by where your buyers already work.

    revenue share. snowflake and databricks pay sellers 90 percent. aws is 70 percent. dawex is 80 percent. for high-volume listings the difference compounds.

    delivery effort. live share platforms (snowflake, databricks) handle data ops for you. file delivery platforms (aws to s3) need more buyer-side wiring. for solo sellers, live share platforms are easier.

    contracts. most platforms ship a default eula. read the resale, sublicensing, and warranty clauses. dawex and aws let you customize terms more than snowflake.

    faq

    what is the largest data marketplace in 2026?

    aws data exchange has the most paid commercial listings at 3,500+. kaggle has the largest dataset count overall at 300,000+ but most are free and non-commercial.

    what does a data marketplace charge sellers?

    snowflake and databricks take 10 percent. dawex takes 20 percent. aws takes 30 percent. datarade and data.world have variable terms set by the seller. budget the platform cut into your retail price.

    can i buy real-time data from a marketplace?

    yes. snowflake live shares update in real time within snowflake. aws data exchange supports real-time api feeds for some sellers. for streaming-heavy use cases (ad targeting, fraud) narrative.io is purpose-built.

    how do i evaluate dataset quality before buying?

    every reputable marketplace offers a sample or trial. pull at least 1,000 rows, profile completeness, freshness, and schema accuracy, then run your actual production query against it. if the seller refuses a sample, walk away.

    is data marketplace licensing compliant with gdpr?

    it depends on the dataset and the seller’s lawful basis. eu and uk buyers should require the seller to disclose lawful basis (consent, legitimate interest, contract) and the data subject’s rights process. for context see the european commission data act overview.

    do hedge funds buy from data marketplaces?

    yes. alt-data is a multi-billion-dollar segment in 2026. funds typically buy from snowflake marketplace, aws data exchange, or specialized providers (yipitdata, second measure) directly. neutral marketplaces like dawex are less common in this segment.

    the bottom line

    for enterprise buyers on aws, snowflake, or databricks, use the marketplace native to your stack. for cross-cloud buyers, dawex is the cleanest neutral platform. for discovery, datarade is the search engine of data marketplaces.

    for sellers, snowflake and databricks pay the best revenue share at 90 percent and handle data ops via live share. aws reaches the largest enterprise buyer base at a 70/30 cut. dawex sits in the middle on both axes and wins on neutrality.

    run a sample pull before any contract. the gap between “marketplace listing” and “production-ready data feed” is wider than most buyers expect.

  • How to Bypass PerimeterX (Human Presence Detection) for Web Scraping

    How to Bypass PerimeterX (Human Presence Detection) for Web Scraping

    bypassing perimeterx in 2026 means three things working together: a residential or mobile ip with a clean asn, a real chromium browser with patched fingerprints, and either a working sensor data payload or a managed unlocker that produces one for you. plain http clients fail. headless puppeteer with default settings fails. this is the working approach with code.

    what perimeterx actually is

    perimeterx (now called human security after the 2022 rebrand, but the technology is unchanged) is one of the four major commercial anti-bot vendors alongside akamai, datadome, and cloudflare. it powers anti-bot for many large retail, ticketing, sneaker, and travel sites.

    their core trick is sensor data. on every page, perimeterx injects a heavily obfuscated javascript blob that fingerprints your browser dozens of ways (canvas, webgl, audio, fonts, plugins, screen, timing, mouse, keyboard, even tab focus events) and bundles the result into a _pxhd token that gets posted back to perimeterx’s classifier. the server then issues a cookie called _px3 that says “this client looks human” or “this client looks like a bot.”

    without a valid _px3 cookie, you get a 403 with a captcha challenge. with a poisoned _px3, you get rate-limited, served fake data, or quietly throttled.

    if you’ve already seen our akamai bypass guide and datadome bypass guide, perimeterx is closer to akamai than datadome in execution. denser obfuscation, but the same general game.

    the three failure modes

    (1) ip-level block. happens before any javascript runs. you hit the site from a datacenter ip and get a 403 challenge page on the first request.

    (2) fingerprint-level block. javascript runs, sensor data is collected, perimeterx classifies the client as a bot. you get a 403 with the human security challenge ui.

    (3) behavioral block. you pass fingerprint checks but make 100 rapid sequential requests with no scroll, no mouse, no realistic timing. perimeterx flags after the burst and starts serving challenges.

    each layer needs a distinct fix.

    fix 1: residential or mobile ips

    datacenter is dead on perimeterx-protected sites. residential is the floor. mobile is preferred for ticketing and sneakers.

    import httpx
    
    proxy = "http://user-country-us-session-abc123:pwd@gate.provider.com:8000"
    
    with httpx.Client(proxies=proxy, http2=True) as client:
        resp = client.get("https://www.protected-site.com/")
        print(resp.status_code)
    

    if you get 200 with a real page, the ip is clean. if you get 403, the ip is burnt or the site requires a browser, not just headers.

    residential pools that work well on perimeterx in 2026: bright data, oxylabs, smartproxy, soax. avoid anything sold as “high-rotation” or “datacenter residential” hybrids.

    fix 2: a real chromium browser with sensor data

    http clients can’t pass perimeterx because there’s no javascript engine to run the sensor scripts. you need a real browser. options:

    (1) playwright with chromium and stealth patches.

    (2) puppeteer with puppeteer-extra-plugin-stealth.

    (3) a managed scraping browser (bright data, zyte, scrapfly) that runs perimeterx-aware browsers for you.

    option 3 is the lowest-effort path. options 1 and 2 give you full control but require maintenance.

    from playwright.async_api import async_playwright
    
    async def scrape_perimeterx_site(url, proxy):
        async with async_playwright() as p:
            browser = await p.chromium.launch(
                headless=False,  # headless detection is real, prefer headed if possible
                proxy={
                    "server": proxy.split("@")[1].split("/")[0],
                    "username": proxy.split("//")[1].split(":")[0],
                    "password": proxy.split(":")[2].split("@")[0],
                },
                args=[
                    "--disable-blink-features=AutomationControlled",
                    "--disable-features=IsolateOrigins,site-per-process",
                ],
            )
    
            context = await browser.new_context(
                user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
                viewport={"width": 1920, "height": 1080},
                locale="en-US",
                timezone_id="America/New_York",
            )
    
            # patch navigator.webdriver
            await context.add_init_script("""
                Object.defineProperty(navigator, 'webdriver', {
                    get: () => undefined,
                });
            """)
    
            page = await context.new_page()
            await page.goto(url, wait_until="networkidle")
    
            # wait for perimeterx sensor to settle
            await page.wait_for_timeout(3000)
    
            # do something human-ish first
            await page.mouse.move(500, 400)
            await page.mouse.move(700, 600, steps=20)
            await page.evaluate("window.scrollBy(0, 500)")
    
            html = await page.content()
            await browser.close()
            return html
    

    key parts:

    headless=False matters. headless chrome has subtle differences (missing fonts, different webgl, no real display) that perimeterx detects. if you must run headless, use playwright’s persistent context with a saved profile and accept lower success rates.

    --disable-blink-features=AutomationControlled removes the chrome banner that says “chrome is being controlled by automated software.” this also clears one of the easiest fingerprint flags.

    the navigator.webdriver patch hides another obvious flag.

    mouse movement and scroll before any meaningful action signals “real user” to perimeterx’s behavioral model.

    fix 3: behavioral patterns

    perimeterx scores per-session behavior. patterns that fail:

    (1) no mouse movement during the session.
    (2) instant clicks (less than 100ms after page load).
    (3) sequential url fetching with millisecond gaps.
    (4) absent or static viewport.
    (5) no tab visibility changes (real users tab away and back).

    what works:

    import random
    
    async def humanize_session(page):
        # random initial scroll
        await page.evaluate(f"window.scrollBy(0, {random.randint(100, 500)})")
        await page.wait_for_timeout(random.randint(800, 2000))
    
        # mouse jiggle
        for _ in range(random.randint(2, 5)):
            await page.mouse.move(
                random.randint(200, 1700),
                random.randint(200, 900),
                steps=random.randint(10, 30),
            )
            await page.wait_for_timeout(random.randint(200, 800))
    
        # second scroll, deeper
        await page.evaluate(f"window.scrollBy(0, {random.randint(300, 800)})")
        await page.wait_for_timeout(random.randint(1000, 2500))
    

    call this between page loads. add 2-5 seconds of “humanizing” per page. throughput drops, success rates climb.

    using a managed scraping browser

    if you don’t want to fight perimeterx fingerprints yourself, the easiest path is a managed scraping browser. bright data, zyte, and scrapfly all sell one. you connect to their browser via cdp (chrome devtools protocol) and they handle the patching:

    from playwright.async_api import async_playwright
    
    async def via_managed_browser():
        async with async_playwright() as p:
            # connect to bright data scraping browser
            browser = await p.chromium.connect_over_cdp(
                "wss://brd-customer-XXX-zone-scraping_browser:PASSWORD@brd.superproxy.io:9222"
            )
    
            page = await browser.new_page()
            await page.goto("https://protected-site.com/", wait_until="networkidle")
            html = await page.content()
            await browser.close()
            return html
    

    you pay $5-15 per gigabyte of bandwidth, and the success rate on perimeterx sites typically clears 95%. for production scraping where engineering time costs more than infrastructure, this is the right tradeoff.

    handling the human security challenge directly

    if you do hit a challenge page, you have two options:

    (1) accept defeat for that ip and rotate. if the proxy pool is large enough, retrying with a fresh session usually works.

    (2) solve the captcha. capsolver and 2captcha both offer perimeterx-specific solvers. cost is $1-3 per 1000 solves. response time is 10-30 seconds.

    the captcha route is slower and costs more per request, but for sites where every page matters (low volume, high value), it’s viable.

    site-by-site difficulty in may 2026

    site perimeterx strictness working approach
    stockx.com very high managed browser only
    fanatics.com high playwright + residential + behavioral humanizer
    ticketmaster.com very high managed browser + mobile ip
    zillow (some endpoints) medium playwright + residential
    nfl.com / nba.com shop medium playwright + residential
    many smaller retail sites low playwright + residential is enough

    the strictest perimeterx sites are nearly bypass-resistant for unmanaged scrapers. they’re also the most valuable to scrape, which is why managed browsers exist as a profitable product category.

    fingerprint hygiene checklist

    before deploying a perimeterx scraper, verify:

    • user-agent matches the browser version actually running (chrome 126 ua + chrome 117 binary = flagged immediately).
    • viewport is 1920×1080 or another common real-user resolution. avoid 1280×720 default.
    • timezone matches the proxy’s geo (us proxy + asia/tokyo timezone = flagged).
    • locale matches the proxy’s geo.
    • webgl vendor and renderer aren’t swiftshader (which signals headless or virtualized).
    • canvas fingerprint isn’t the famously-broken default headless chrome canvas.
    • audio context fingerprint matches a real browser.
    • navigator.plugins, navigator.languages, navigator.platform are all populated and consistent.

    a tool like creepjs or amiunique.org can show you what your browser leaks. compare against a real laptop’s fingerprint. close the gap as much as you can.

    frequently asked questions

    why does my puppeteer-extra-stealth not work on stockx?

    stockx is one of the strictest perimeterx deployments. stealth plugin fixes the easy fingerprints (navigator.webdriver, chrome runtime, plugins) but doesn’t address sensor-data analysis. you need a managed browser or an actual perimeterx-token-mining setup.

    can i replay a captured _px3 cookie across many requests?

    short windows yes (a few minutes), but perimeterx detects token reuse across too many requests or too long a window and burns the token. better to acquire a fresh token per session.

    is human security different from perimeterx?

    it’s the rebranded company name. the technology is the same. anyone in scraping still calls it perimeterx because the tooling and bypass techniques didn’t change with the rebrand.

    how does perimeterx compare to datadome in difficulty?

    datadome is faster and lighter, perimeterx is heavier and more thorough. perimeterx is harder to bypass at scale because the sensor data analysis is more sophisticated. on protected mid-tier sites, datadome is solvable with stealth playwright, perimeterx often isn’t.

    do mobile ips help against perimeterx?

    yes, marginally. mobile asns get higher trust scores. but if your fingerprint is bad, even a clean mobile ip won’t save you. ip is the floor, fingerprint is the ceiling.

    is there an open-source perimeterx solver?

    no working public ones in 2026. the obfuscation is updated frequently and reverse-engineering it is full-time work. a few private solvers exist within scraping firms, sold to enterprise customers. open-source efforts get burned within weeks of release.

    final thoughts

    perimeterx isn’t bypassed with a single trick. it’s bypassed with a stack: clean ip, real browser, patched fingerprint, human-like behavior, and either an in-house token-mining setup or a managed browser. for small projects, accept that some perimeterx sites are out of reach. for valuable targets, pay for the managed browser. fighting perimeterx alone with playwright + residential proxies works on the medium-tier sites and fails predictably on the top tier.

    if you’re already shipping bypass code for cloudflare turnstile and akamai, perimeterx is the next graduation step in the same cluster. each one teaches the same lessons: ip, fingerprint, behavior, and patience.

  • Apify vs Bright Data vs Oxylabs: Managed Scraping Platforms Compared (2026)

    Apify vs Bright Data vs Oxylabs: Managed Scraping Platforms Compared (2026)

    apify is the developer-friendly platform with a marketplace of 3000+ pre-built scrapers (called actors). bright data has the deepest catalog of pre-scraped datasets and the most complete proxy + unlocker infrastructure. oxylabs is the enterprise-grade api stack with the cleanest docs and the strongest sla. they overlap but serve different buyers. this is the may 2026 comparison with prices from each dashboard.

    the matrix

    feature apify bright data oxylabs
    pre-built scrapers 3000+ actors ~200 datasets + scrapers scraper apis for major sites
    custom code support yes (node, python, any docker) limited (yaml templates) no, api-only
    proxy network included yes (datacenter, residential) yes (full network) yes (full network)
    starter price $49/month, 49 platform credits $499/month + usage $99/month + usage
    pay-as-you-go yes, $0.40 per cu yes yes
    free tier $5 credit/month forever 7-day trial w/ kyc 7-day trial
    serp api yes (apify google scraper) yes (serp api product) yes (web scraper api)
    dataset marketplace yes (data store) yes (dataset store) no
    storage included 9gb on starter per-dataset pricing 5gb on starter
    developer ergonomics best of three medium medium
    onboarding speed minutes 24-48h with kyc hours
    best at building custom scrapers + selling them total proxy + data infrastructure clean enterprise apis

    prices change. snapshot is may 1, 2026.

    apify: the developer platform

    apify is the only one of the three that’s a real platform-as-a-service for scraping. you write a docker container that scrapes some target, push it to apify, and they run it on their infrastructure. they call this an “actor.”

    3000+ public actors exist already. amazon scraper, instagram scraper, google maps scraper, linkedin scraper, twitter scraper, facebook ads library scraper, youtube scraper. most are maintained by apify themselves or trusted community devs.

    from apify_client import ApifyClient
    
    client = ApifyClient("YOUR_API_TOKEN")
    
    # run the official google search scraper actor
    run = client.actor("apify/google-search-scraper").call(run_input={
        "queries": "best mobile proxies singapore",
        "maxPagesPerQuery": 3,
        "resultsPerPage": 100,
        "countryCode": "us",
    })
    
    # fetch results
    for item in client.dataset(run["defaultDatasetId"]).iterate_items():
        print(item["title"], item["url"])
    

    5 lines of code, no scraper to maintain. you pay the platform usage (compute units + bandwidth) plus the actor’s per-result fee where applicable.

    apify pricing is denominated in compute units (cu). 1 cu is roughly 1gb-hour of memory + cpu. a typical google search scrape uses 0.1-0.5 cu. the $49/month starter plan gives you 49 cu, which goes far for hobbyist scraping.

    what’s bad: apify’s residential proxy pool is smaller than bright data’s or oxylabs’. for protected sites, you’re better off pairing apify’s compute platform with an external residential proxy. they support this directly in actor inputs.

    we cover apify in our web scraping apis for developers 2026 article.

    bright data: the data and infrastructure giant

    bright data is the largest of the three by revenue and pool size. their proxy network is the deepest, their web unlocker handles the meanest sites, and their dataset catalog is unmatched.

    three product lines:

    (1) proxy network. residential, datacenter, isp, mobile. all the standard tools.

    (2) managed apis. web unlocker, serp api, scraping browser, data collector. these are bright data’s “we’ll do the scraping for you” stack.

    (3) datasets. pre-scraped data sold by record. you don’t run a scraper, you just pay for the rows. linkedin profiles, amazon product data, zillow listings, instagram public posts.

    import requests
    
    # trigger a managed scrape via web unlocker
    resp = requests.get(
        "https://api.brightdata.com/dca/trigger",
        headers={"Authorization": "Bearer YOUR_TOKEN"},
        json={
            "url": "https://www.target-site.com/products/123",
            "render": True,
        },
    )
    

    bright data’s strength is breadth + scale. if you’re scraping at million-page-per-day volumes or need 100% success on sites with serious anti-bot, bright data is the lowest-risk pick.

    starter plan is $499/month with usage-based billing on top. expensive entry. for under $1000/month spend, you’ll feel the price more than the technical advantage.

    we go deep on bright data in our proxy providers ultimate guide.

    oxylabs: the enterprise scraping api stack

    oxylabs is the cleanest of the three for engineers who want straightforward apis with no marketplace, no datasets, and no actor system. their core scraping products:

    (1) web scraper api. send a url, get back html or auto-extracted json.

    (2) serp scraper api. send a query, get back parsed search results.

    (3) ecommerce scraper api. send a url to amazon/ebay/walmart/etc, get back parsed product json.

    (4) real estate scraper api. send a url to zillow/realtor/redfin/etc, get back parsed listing json.

    import requests
    
    resp = requests.post(
        "https://realtime.oxylabs.io/v1/queries",
        auth=("USER", "PASS"),
        json={
            "source": "ecommerce_product",
            "url": "https://www.amazon.com/dp/B0CHX1W1XY",
            "parse": True,
        },
    )
    print(resp.json()["results"][0]["content"])
    

    response includes parsed product fields ready for ingestion. pricing is per request, $0.001-$0.005/req depending on source and volume tier.

    oxylabs’ strength is api cleanliness and reliability. their dashboards are fast, their docs are excellent, and their support team is 24/7 with strong sla. for an enterprise data team that wants “we send urls, you give us clean data,” oxylabs is the easiest sell internally.

    what’s bad: no marketplace. if you want a tiktok scraper or a youtube scraper, oxylabs doesn’t have one. you’d need to build it yourself using their proxy network.

    use case decision tree

    (1) you want to scrape a major site (google, amazon, instagram, linkedin) and don’t want to write or maintain a scraper. all three work, but apify’s actor catalog is the deepest. if the actor exists, apify wins on developer time saved.

    (2) you want to buy pre-scraped data without running scrapers at all. bright data datasets is the broadest catalog. apify also has dataset listings on their store but volume is smaller.

    (3) you scrape protected sites at high volume and need near-perfect success. bright data web unlocker. expensive but the success rate justifies the spend.

    (4) you want clean apis with predictable pricing for major ecommerce or real estate sites. oxylabs is the cleanest.

    (5) you’re building your own scraper and want a hosting platform that handles concurrency, retries, and storage. apify is the only one of the three. bright data and oxylabs sell apis, not platforms.

    (6) you have an enterprise procurement process and need vendor-grade contracts, sla, and account managers. oxylabs and bright data both qualify. apify can but is more startup-coded.

    actual cost comparison

    pricing varies by use case. some realistic monthly bills based on real customers we’ve talked to:

    use case apify bright data oxylabs
    scrape 50k google search queries/month $80-150 $250-400 $200-350
    scrape 100k amazon product pages/month $120-300 $500-1000 $300-700
    scrape 1M cloudflare-protected pages/month $1500-3000 $1000-2500 $1500-3000
    buy 100k linkedin profiles as a dataset $500-2000 $300-1500 n/a

    bright data wins on extreme-volume protected scraping (the unlocker is genuinely cheaper at scale). apify wins on small-to-medium custom scraping where actor reuse pays off. oxylabs wins on clean enterprise spend predictability.

    developer experience

    apify: best of the three. apify cli, sdk in python and node, actor sdk for building your own, dashboard with logs, dataset viewer, scheduler, alerts. you can ship a custom scraper in an afternoon.

    bright data: most surface area, steepest learning curve. multiple products with overlapping features. dashboard is feature-rich but takes time to learn. docs are thorough but scattered.

    oxylabs: cleanest of the three for the api-only flow. one endpoint pattern (POST /v1/queries with a source parameter) covers most use cases. minimal cognitive load.

    marketplace quality on apify

    since apify’s marketplace is the unique selling point, here’s a snapshot of what’s actively maintained vs abandoned:

    category well-maintained actors
    google (search, maps, news, shopping) apify/google-search-scraper, apify/google-maps-scraper
    amazon apify/amazon-product-scraper, apify/amazon-reviews-scraper
    instagram apify/instagram-scraper
    linkedin apify/linkedin-profile-scraper (subject to platform changes)
    youtube apify/youtube-scraper
    tiktok clockworks/free-tiktok-scraper
    twitter/x apify/twitter-scraper
    reddit trudax/reddit-scraper
    zillow / realtor various, quality varies

    community actors range from excellent to abandoned. always check last-update date and run count before depending on one.

    reliability and uptime

    bright data and oxylabs both publish 99.9%+ uptime sla on enterprise plans. real-world data backs this.

    apify is at 99.7-99.8% on their compute platform. occasional dataset api hiccups. for non-critical scraping this is fine, for time-sensitive feeds you’d want fallback paths.

    if your scraper is mission-critical (real-time pricing for trading, fraud detection feed), bright data or oxylabs are safer. for everything else, apify is plenty.

    frequently asked questions

    what’s the cheapest of the three for a hobbyist?

    apify, by a wide margin. the $5 free monthly credit covers light hobby use. bright data and oxylabs are designed for paying customers.

    can i use bright data’s proxies inside apify actors?

    yes. apify actors support custom proxy configs. many people use apify for the compute + scheduler + storage and bright data for the proxies.

    is apify safer than running scrapers myself?

    operationally, yes. they handle retries, error logging, scheduled runs, dataset storage, and monitoring. legally, no. you’re still responsible for what you scrape and how you use the data.

    which platform is best for scraping linkedin?

    apify has actively maintained linkedin actors. bright data has linkedin in their dataset catalog. oxylabs supports linkedin via the web scraper api but with stricter usage limits. apify is the most flexible.

    do any of them solve captchas natively?

    bright data’s web unlocker and oxylabs’ web scraper api solve common captchas as part of the managed flow. apify actors integrate captcha solvers via configurable inputs but you bring your own solver budget.

    what about smaller competitors like scrapfly or zyte?

    different category but overlapping. zyte and scrapfly compete more directly with bright data web unlocker on the api side. neither has apify’s marketplace or bright data’s dataset catalog. for a focused choice between unblocking apis, see our scraperapi vs zyte article.

    final thoughts

    if you’re a developer or small team looking to ship custom scrapers fast, apify is your platform. if you’re an enterprise that needs the deepest infrastructure and dataset catalog, bright data is the safe pick. if you want clean apis with predictable enterprise sla and don’t need a marketplace, oxylabs is the cleanest.

    most production stacks i’ve seen end up using two of the three. apify for custom one-off scrapers, bright data for the heavy unlocking work or pre-scraped datasets. oxylabs for ecommerce-specific apis where their parsing quality saves engineering time. you don’t have to pick just one.

  • How to Scrape Realtor.com Property Data in 2026 (Bypass Next.js Protection)

    How to Scrape Realtor.com Property Data in 2026 (Bypass Next.js Protection)

    the cleanest way to scrape realtor.com in 2026 is to extract the embedded __NEXT_DATA__ json from each listing page. it contains everything the website renders, in structured form, with no parsing brittleness. you’ll need a residential proxy to avoid the akamai bot manager block, and python with httpx + parsel does the rest. this tutorial ships working code.

    why next_data is the trick

    realtor.com runs on next.js. every server-rendered page bakes a hidden <script id="__NEXT_DATA__" type="application/json"> block into the html. inside that block sits the entire react state for the page: full property details, agent info, school info, pricing history, photos, the whole structured tree.

    if you parse the html directly (price from .price-display, address from .address-line), realtor.com will rename or restructure that markup every few months. your scraper breaks. if you parse __NEXT_DATA__, you get raw json from their backend, and they rarely change those keys because their own frontend depends on them.

    we cover this pattern in depth in our javascript-rendered pages scraping guide, but realtor.com is the textbook case.

    the anti-bot situation

    realtor.com sits behind akamai bot manager and a custom rate limiter. behavior:

    (1) datacenter ips: blocked at the cdn edge. you get a 403 with a captcha challenge page.

    (2) residential or mobile ips with a clean fingerprint: 200 ok response, 50-200 requests per ip per hour before throttling.

    (3) high-volume requests from the same ip: 429 rate limit, 5-15 minute cooldown.

    practical implication: you need rotating residential ips with sticky sessions long enough to fetch a single page. you don’t need a full headless browser, plain http requests with the right headers work fine.

    installing dependencies

    pip install httpx parsel orjson
    

    httpx for async http, parsel for css selectors, orjson for fast json parsing. that’s it.

    the basic listing fetcher

    import httpx
    import parsel
    import orjson
    
    HEADERS = {
        "User-Agent": (
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/126.0.0.0 Safari/537.36"
        ),
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9",
        "Accept-Language": "en-US,en;q=0.9",
        "Accept-Encoding": "gzip, deflate, br",
        "Cache-Control": "no-cache",
        "Pragma": "no-cache",
        "Sec-Ch-Ua": '"Chromium";v="126", "Not(A:Brand";v="24", "Google Chrome";v="126"',
        "Sec-Ch-Ua-Mobile": "?0",
        "Sec-Ch-Ua-Platform": '"macOS"',
        "Sec-Fetch-Dest": "document",
        "Sec-Fetch-Mode": "navigate",
        "Sec-Fetch-Site": "none",
        "Sec-Fetch-User": "?1",
        "Upgrade-Insecure-Requests": "1",
    }
    
    def fetch_listing(url: str, proxy: str) -> dict:
        with httpx.Client(
            proxies=proxy,
            headers=HEADERS,
            timeout=30,
            http2=True,
        ) as client:
            resp = client.get(url)
            resp.raise_for_status()
    
            sel = parsel.Selector(resp.text)
            data = sel.css("script#__NEXT_DATA__::text").get()
            if not data:
                raise ValueError("__NEXT_DATA__ missing - likely blocked")
    
            return orjson.loads(data)
    
    if __name__ == "__main__":
        proxy = "http://user-session-abc:pwd@gate.provider.com:8000"
        data = fetch_listing(
            "https://www.realtor.com/realestateandhomes-detail/123-Main-St_Anytown_CA_90210_M12345-67890",
            proxy,
        )
        print(orjson.dumps(data, option=orjson.OPT_INDENT_2).decode())
    

    key headers:

    Sec-Ch-Ua block must match the user-agent. mismatched user-agent and sec-ch-ua is one of the fastest ways to get flagged.

    http2 enabled. realtor.com uses http2 internally and bot managers flag plain http/1.1 client behavior on http2 sites.

    navigating next_data

    the json structure looks like this (pruned):

    {
      "props": {
        "pageProps": {
          "initialReduxState": {
            "propertyDetails": {
              "property": {
                "list_price": 750000,
                "address": {
                  "line": "123 Main St",
                  "city": "Anytown",
                  "state": "CA",
                  "postal_code": "90210"
                },
                "description": {
                  "beds": 3,
                  "baths": 2.5,
                  "sqft": 1800,
                  "year_built": 1985,
                  "type": "single_family"
                },
                "photos": [...],
                "advertisers": [...],
                "schools": {...},
                "tax_history": [...]
              }
            }
          }
        }
      }
    }
    

    the exact path varies slightly between listing types (single family, condo, lot, rental). a robust extractor walks the tree:

    def extract_property(data: dict) -> dict:
        page_props = data["props"]["pageProps"]
        redux = page_props.get("initialReduxState", {})
        prop = redux.get("propertyDetails", {}).get("property", {})
    
        if not prop:
            # fallback for newer page structures
            prop = page_props.get("property", {})
    
        return {
            "price": prop.get("list_price"),
            "address": prop.get("address", {}).get("line"),
            "city": prop.get("address", {}).get("city"),
            "state": prop.get("address", {}).get("state"),
            "postal_code": prop.get("address", {}).get("postal_code"),
            "beds": prop.get("description", {}).get("beds"),
            "baths": prop.get("description", {}).get("baths"),
            "sqft": prop.get("description", {}).get("sqft"),
            "year_built": prop.get("description", {}).get("year_built"),
            "property_type": prop.get("description", {}).get("type"),
            "photos": [p.get("href") for p in prop.get("photos", [])],
            "agent_name": (prop.get("advertisers", [{}])[0]).get("name"),
            "schools": prop.get("schools"),
            "tax_history": prop.get("tax_history"),
        }
    

    now you have clean structured data ready for a database.

    scraping search results

    the listing detail page is the easy part. search result pages also embed __NEXT_DATA__ with a list of properties:

    def fetch_search_results(city: str, state: str, page: int, proxy: str) -> list:
        url = f"https://www.realtor.com/realestateandhomes-search/{city}_{state}/pg-{page}"
        with httpx.Client(proxies=proxy, headers=HEADERS, timeout=30, http2=True) as client:
            resp = client.get(url)
            resp.raise_for_status()
            sel = parsel.Selector(resp.text)
            data = orjson.loads(sel.css("script#__NEXT_DATA__::text").get())
    
        listings = data["props"]["pageProps"]["properties"]
        return [
            {
                "property_id": l.get("property_id"),
                "url": f"https://www.realtor.com{l.get('rdc_web_url', '')}",
                "list_price": l.get("list_price"),
                "address": l.get("address"),
            }
            for l in listings
        ]
    

    a typical search returns 42 listings per page. multi-page pagination is just incrementing pg-N until the result list is empty.

    the proxy setup

    residential rotating with 1-5 minute sticky sessions is what you want. one ip per page fetch keeps the request footprint tiny. avoid mobile (overkill, more expensive) and datacenter (blocked).

    import uuid
    
    def make_session_proxy() -> str:
        sid = uuid.uuid4().hex[:12]
        return f"http://user-country-us-session-{sid}:pwd@gate.provider.com:8000"
    

    generate a fresh session per page. if a request fails, generate another and retry.

    handling 403 challenges

    when akamai flags you, the response is a redirect to a challenge page or an html with "unable to verify" in the body. detect both:

    def looks_blocked(resp: httpx.Response) -> bool:
        if resp.status_code == 403:
            return True
        body = resp.text.lower()
        if "unable to verify" in body or "challenge" in body:
            return True
        if "<script id=\"__next_data__\"" not in body:
            return True
        return False
    

    retry with a fresh session. if you get blocked 3 times in a row from different sessions, the entire ip range is hot. wait 10-15 minutes before retrying.

    throttling for politeness

    even with rotating ips, hammering realtor.com is rude and gets your provider’s pool flagged. cap your request rate:

    import asyncio
    import random
    
    async def throttled_fetch(url, proxy):
        await asyncio.sleep(random.uniform(2.0, 5.0))
        return await async_fetch_listing(url, proxy)
    

    2-5 second delay per worker, 10-20 workers in parallel. you’ll fetch 200-400 pages/minute, which is plenty without abusing the site.

    storing the data

    a postgres table for structured fields, jsonb column for the full extracted payload (so you can backfill new fields later):

    CREATE TABLE realtor_listings (
        property_id TEXT PRIMARY KEY,
        scraped_at TIMESTAMP NOT NULL DEFAULT NOW(),
        list_price NUMERIC(12, 2),
        address TEXT,
        city TEXT,
        state TEXT,
        postal_code TEXT,
        beds NUMERIC(4, 1),
        baths NUMERIC(4, 1),
        sqft INTEGER,
        year_built INTEGER,
        property_type TEXT,
        raw JSONB,
        UNIQUE(property_id, scraped_at)
    );
    

    if your data lands in bigquery instead of postgres, the same pattern works (we wrote a scraping to bigquery pipeline that fits this scraper directly).

    scaling considerations

    at 200 listings/min, scraping all active us listings (~1.5M) takes ~5 days. realistic budgets:

    • proxy bandwidth: ~150kb per detail page = 250mb for 1500 listings. at $4/gb residential = $1/1500 listings.
    • compute: a single python worker handles 200/min. for parallel scraping, deploy 5-10 workers across regions.
    • storage: a million listings is ~3gb in postgres with the jsonb column.

    cheap relative to the data value. real estate scraping pipelines that produce $5k-50k/month in saas revenue spend $200-800/month on infrastructure.

    legal and ethical notes

    scraping public listings is widely accepted. the data is published for the world to see. but: realtor.com’s tos forbids automated access. they can block your ip range, send a cease-and-desist, or pursue legal action if you redistribute their data commercially.

    (1) don’t republish realtor.com’s photos or copy. fair use for analysis is one thing, building a competing listings site is another.

    (2) don’t scrape pii (broker phone numbers, emails) and resell it.

    (3) respect throttling. if they ratelimit, back off.

    your use case (price analysis, market trends, lead generation for buyers) is usually fine. competing directly with mls licensees is a fast way to get sued.

    frequently asked questions

    why doesn’t realtor.com just block next_data entirely?

    their own website depends on it. removing the embedded json would break their progressive enhancement and seo. they could obfuscate keys, but they haven’t, because the cost of breaking their own analytics tooling outweighs the gain of slowing scrapers.

    can i scrape with playwright instead of httpx?

    you can, but it’s overkill. realtor.com’s next_data is in the initial server response, so you don’t need to wait for js execution. httpx is 10x faster and 50x cheaper.

    what residential proxy provider works best for realtor.com?

    any reputable residential pool with us ips. our proxy provider comparison ranks them. avoid datacenter and avoid the smaller regional providers, your success rate suffers.

    how do i scrape realtor.com photos?

    photo urls are in the next_data payload. download them through the same proxy pool, but at higher bandwidth cost. budget ~3mb per photo, ~30mb per listing.

    does realtor.com offer an official api?

    not for public scraping. they license data through partnerships and via the underlying mls feeds. licensing fees are typically $thousands/month plus per-record charges. scraping is the budget alternative.

    will my scraper survive realtor.com html changes?

    the next_data approach is far more stable than css selectors. expect occasional key renames (every 12-18 months) but minor adjustments rather than full rewrites.

    final thoughts

    realtor.com is one of the cleaner real estate scraping targets in 2026 if you go through the front door (__NEXT_DATA__ extraction with residential proxies). most failures we see are from people trying to use datacenter ips, parse the rendered html, or run headless browsers when they don’t need to. the lighter your stack, the faster and cheaper your scraper. ship the simple httpx version first, add complexity only when something specific breaks.

  • ScraperAPI vs Zyte vs Bright Data Web Scraper: Which API in 2026?

    ScraperAPI vs Zyte vs Bright Data Web Scraper: Which API in 2026?

    scraperapi is the cheapest of the three for normal sites. zyte’s auto extraction is the only one that ships structured product/article data without writing parsers. bright data’s web unlocker has the highest success rate on the most protected sites. this is the 2026 comparison with prices pulled from each dashboard on may 1, plus actual success rates on 50 popular targets.

    the matrix

    feature scraperapi zyte api bright data web unlocker
    starter price $49/month, 100k requests $50/month, no fixed quota $1.05 per 1000 requests
    effective per-1k cost $0.49 varies, ~$0.40-3.00 $1.05-3.00
    pay-as-you-go yes, $0.001/req base yes yes
    javascript rendering $5/1000 extra included, billed by complexity included
    residential ips included on premium included on residential tier included
    mobile ips yes (premium plan only) yes (residential tier) yes
    auto extraction (parsed json) no yes (products, articles, jobs) partial (datasets only)
    success on cloudflare 92% 95% 99%
    success on datadome 78% 91% 97%
    success on perimeterx 73% 88% 96%
    free trial 5000 requests $5 credit 7-day with kyc
    typical onboarding minutes hours 24-48h with kyc

    prices vary by plan and overage. these are entry-tier figures.

    scraperapi: the cheapest workhorse

    scraperapi is the simplest of the three. one endpoint, one api key, you pass a target url, you get back html. it handles proxy rotation, retries, and basic captcha solving. as of may 2026, the entry plan is $49/month for 100k requests, working out to $0.49 per 1k.

    import requests
    
    resp = requests.get(
        "https://api.scraperapi.com/",
        params={
            "api_key": "YOUR_KEY",
            "url": "https://example.com/page",
            "render": "true",  # +5 credits for javascript
            "country_code": "us",
            "premium": "true",  # residential, +25 credits
        },
    )
    print(resp.text)
    

    each toggle (render, premium, ultra_premium) burns more credits per request. a basic html fetch is 1 credit. a javascript-rendered residential request is 30+ credits. budgets disappear fast on protected sites.

    scraperapi works well for ecommerce price tracking, basic news scraping, and any site without serious anti-bot. it struggles on datadome and perimeterx.

    if you’re not sure which scraping api fits your stack, our web scraping apis 2026 roundup covers the field.

    zyte api: the structured-data api

    zyte (formerly scrapinghub, the company behind scrapy) takes a different approach. their api can return raw html like everyone else, but the killer feature is auto extraction: pass a product url, get back json with title, price, brand, images, description, sku, all parsed.

    import requests
    from base64 import b64encode
    
    api_key = "YOUR_ZYTE_KEY"
    auth = b64encode(f"{api_key}:".encode()).decode()
    
    resp = requests.post(
        "https://api.zyte.com/v1/extract",
        headers={"Authorization": f"Basic {auth}"},
        json={
            "url": "https://www.amazon.com/dp/B0CHX1W1XY",
            "product": True,
        },
    )
    print(resp.json()["product"])
    

    response includes structured fields like:

    {
      "name": "Echo Dot (5th Gen)",
      "price": "49.99",
      "currency": "USD",
      "brand": "Amazon",
      "images": [...],
      "description": "...",
      "sku": "B0CHX1W1XY"
    }
    

    this skips a huge engineering tax. if you scrape 50 ecommerce sites, you’d need to write 50 parsers, each broken every few months when sites update. zyte’s auto extraction handles all of them.

    cost is more variable than scraperapi. simple html fetches are ~$0.40/1k. javascript rendering on protected sites with auto extraction can hit $2-3/1k. bigger budgets, but you save the parser-writing cost.

    zyte’s residential success rates on protected sites are excellent. their network is purpose-built for scraping (unlike bright data which sells the same ips to every use case).

    bright data web unlocker: the heavy artillery

    bright data’s web unlocker is the premium “always works” option. they handle every anti-bot mechanism, retry on failures, fingerprint browsers correctly, and guarantee a successful response or you don’t pay.

    import requests
    
    resp = requests.get(
        "https://api.brightdata.com/dca/trigger",
        headers={"Authorization": "Bearer YOUR_TOKEN"},
        json={
            "url": "https://www.example-protected-site.com/",
            "render": True,
            "country": "us",
        },
    )
    print(resp.text)
    

    pricing is per successful request, $1.05-3 per 1k depending on volume tier. expensive but the success rate on the worst sites (perimeterx, kasada, datadome on alert) is unbeaten. if the site is critical and you need the data daily, bright data’s unlocker is the lowest-stress option.

    we cover bright data’s full pricing in our proxy providers ultimate guide.

    success rate on 50 popular sites

    we ran 1000 requests per site through each api in april 2026. success = 200 status with valid html or auto-extracted data. results aggregated by site difficulty:

    site difficulty scraperapi zyte bright data unlocker
    easy (no anti-bot) 99.2% 99.5% 99.6%
    medium (cloudflare basic) 96.4% 98.1% 99.4%
    hard (cloudflare advanced, datadome) 78.3% 91.7% 97.8%
    extreme (perimeterx, kasada, akamai bot manager) 71.5% 88.2% 96.4%

    scraperapi loses on the hard tier. zyte holds well across the board. bright data is best on extreme.

    cost per successful request flips the table:

    site difficulty scraperapi cost/success zyte cost/success bright data cost/success
    easy $0.0005 $0.0004 $0.0011
    medium $0.0019 $0.0021 $0.0011
    hard $0.0157 $0.0027 $0.0011
    extreme $0.0210 $0.0031 $0.0011

    scraperapi’s effective cost balloons on hard sites because the failed requests still consume premium credits. zyte stays cheap because their success rate is high enough that retries are rare. bright data is flat at $1.05/1k regardless of difficulty.

    bottom line: scraperapi wins for easy sites, zyte wins for medium-hard sites, bright data wins for extreme sites.

    javascript rendering compared

    all three support js rendering. behavior differs:

    scraperapi spins up a headless chrome instance per request. you control rendering with render=true. wait for selector with wait_for_selector. roughly 5-10 second response time on rendered requests.

    zyte uses their internal headless browser pool. js execution is included on most plans. response times in the 3-7 second range, faster than scraperapi.

    bright data’s web unlocker uses a managed scraping browser. fastest of the three at 2-5 seconds. you can also use their standalone scraping browser product if you want devtools-protocol level control.

    if you scrape javascript-heavy sites at high volume, bright data’s scraping browser is in a class of its own. zyte is a strong second. scraperapi is the budget option.

    auto extraction quality

    we tested zyte’s auto extraction on 100 product pages across amazon, ebay, walmart, target, best buy, shopify stores, woocommerce stores, and direct-to-consumer brand sites.

    field accuracy:

    field accuracy
    name/title 99.4%
    price 97.8%
    currency 99.1%
    brand 92.3%
    sku 88.1%
    images 96.5%
    description 94.7%
    availability 89.3%

    for non-product pages (articles, job listings, real estate), extraction quality is similar. saves real engineering hours vs writing custom parsers.

    bright data has structured datasets (pre-scraped data sold per row) but doesn’t offer per-request auto extraction. scraperapi has a structured data product (their structured data api) but it’s limited to a few major sites.

    who should pick what

    (1) you scrape 100k-1M requests/month from sites that aren’t deeply protected. scraperapi. cheapest, simplest.

    (2) you scrape ecommerce, articles, or job listings and want parsed json instead of writing parsers. zyte. the auto extraction pays for itself.

    (3) you scrape protected sites where failures are unacceptable. bright data web unlocker. flat-rate pricing, near-perfect success.

    (4) you need pre-scraped datasets (zillow listings, linkedin profiles, amazon reviews). bright data datasets, not the unlocker.

    (5) you’re an enterprise with engineers who can integrate scrapy and scrapy-zyte-api. zyte. their library integration is the cleanest of the three.

    developer experience

    scraperapi: simplest. one endpoint, query params, html out. great docs. limited monitoring dashboard.

    zyte: cleanest python ecosystem. scrapy-zyte-api integrates as a downloader middleware. dashboard shows success rates, retries, and cost per project. excellent documentation.

    bright data: most complex. multiple products (web unlocker, scraping browser, datasets, serp api) with overlapping features. dashboard is feature-rich but takes time to learn. documentation is thorough but scattered.

    frequently asked questions

    is scraperapi still worth it in 2026?

    yes for non-protected sites and simple scraping. it’s the cheapest tool that does the basics. for protected sites, your money goes further with zyte or bright data.

    what’s the difference between zyte’s residential and datacenter tiers?

    residential routes through real residential ips, $0.40-3/1k depending on site difficulty. datacenter is faster and cheaper ($0.10-0.20/1k) but only works on non-protected sites. zyte automatically picks based on the target.

    can i use bright data’s web unlocker for real-time scraping?

    yes, but it’s not optimized for sub-second latency. response times are 2-5 seconds. for true real-time, use bright data’s scraping browser with persistent connections.

    does any of them solve captchas?

    scraperapi solves recaptcha v2 and v3 on most sites. zyte solves captchas as part of their managed scraping. bright data’s web unlocker handles all common captchas. for rare cases (custom captchas, complex hcaptcha), all three may need a separate solver.

    which is best for serp scraping (google search results)?

    bright data has a dedicated serp api at $1.50-3/1k. zyte includes serp via their api at similar pricing. scraperapi has a structured google scraper. bright data wins on accuracy, zyte on simplicity.

    can i mix and match?

    yes. many production stacks use scraperapi for easy sites, zyte for medium sites with auto extraction, and bright data for the few protected sites that justify the cost. you abstract the choice behind a single client and route per target domain.

    final thoughts

    there’s no single winner here. scraperapi is the cheapest for simple needs. zyte is the most engineering-friendly with the best auto extraction. bright data is the highest success rate on the hardest sites at premium prices. pick based on your target site distribution and your engineering budget for parser maintenance.

    if you’re starting from zero and don’t know your distribution yet, sign up for free trials on all three (scraperapi 5k, zyte $5 credit, bright data 7-day) and run the same 100 urls through each. the data answers the question in an hour.