Author: Xavier Fok

  • GDPR compliance for web scraping in 2026: a practical guide

    GDPR compliance for web scraping in 2026: a practical guide

    GDPR web scraping compliance is the single most-cited legal blocker that engineering teams hit when they try to scale data collection across European targets. The regulation does not ban scraping. It does not even mention scraping. What it does is impose a strict regime on how personal data is collected, processed, and stored, and almost every meaningful scraping project sweeps up at least some personal data along the way. The result is a gap between what teams think they are doing (collecting public information) and what regulators see (processing personal data without a documented lawful basis). This guide walks through what GDPR actually says, how the EU enforcement environment shifted in 2024 and 2025, what scraping operators in 2026 must put in place to stay defensible, and where the live court rulings draw the line.

    The guide is built for technical leads, data engineers, and product owners who already run or are planning a scraping pipeline that touches EU traffic. It is not a substitute for counsel. It is a working framework so you walk into that conversation with the right map.

    What GDPR actually covers in scraping context

    The General Data Protection Regulation (Regulation (EU) 2016/679) applies to the processing of personal data of individuals in the EU and EEA, regardless of where the processor sits. That extraterritorial reach (Article 3) is the part most non-EU teams underestimate. If you scrape a dataset that contains EU residents’ personal data, GDPR applies even if your servers are in Singapore, your team is in the US, and the website you scraped is hosted in Brazil.

    Personal data under Article 4(1) is any information relating to an identified or identifiable natural person. The bar is very low. A name, an email, a username, an IP address, a cookie identifier, a device fingerprint, a profile photo, a location signal, a job title combined with a company, even a forum post that reveals an opinion attached to a pseudonym that can be re-identified, all of these qualify. The European Data Protection Board (EDPB) has consistently taken an expansive view. If a human can plausibly be re-identified from your dataset, even after combination with other public sources, the data is personal.

    Processing under Article 4(2) is also broad: collection, storage, structuring, retrieval, dissemination, alignment, and erasure all count. Scraping is collection. Storing the result in a database is storage. Running it through an LLM is processing. Sending it to a customer is dissemination. Each step needs a lawful basis.

    The lawful bases are set out in Article 6. The two that matter for scraping are consent (almost always impossible to obtain at scrape time) and legitimate interest (the workhorse for B2B and research scraping, but it requires a documented Legitimate Interest Assessment, often called an LIA). The remaining bases (contract, legal obligation, vital interest, public interest) rarely apply.

    For a wider tour of the personal-versus-public-data question, see the personal vs public data scraping framework. For US-side parallels, the CCPA compliance guide for scrapers is the right next read.

    The legitimate interest pathway in practice

    Article 6(1)(f) allows processing where it is necessary for the legitimate interests pursued by the controller, except where those interests are overridden by the rights and freedoms of the data subject. That balancing test is the entire game.

    A practical LIA has three parts. First, the purpose test: is the interest you are pursuing genuine, lawful, and clearly articulated? Market research, fraud prevention, journalism, and competitive intelligence all generally pass. Mass profile harvesting for cold outbound spam usually does not.

    Second, the necessity test: can the same outcome be achieved with less personal data? If your downstream use case only needs aggregated counts, you should not be storing names. If you need company-level signals, you should not be storing individual contributor profiles. Data minimisation under Article 5(1)(c) is not optional.

    Third, the balancing test: do the data subjects’ interests, rights, or fundamental freedoms override yours? This is where you weigh expectations. A user who posted on a public forum reasonably expects that post to be readable by other humans. They probably do not expect it to be ingested by a competitor’s LLM training pipeline at industrial scale and re-surfaced in unrelated contexts. The test is contextual.

    Document the LIA. Date it. Sign it. Re-run it whenever the scope, source list, or downstream use changes. EU regulators in 2025 increasingly asked for the LIA on first contact during investigations, and absence of one is itself evidence of non-compliance.

    Compliance checklist for scraping operators

    Control What it requires Why it matters
    Lawful basis documented Written LIA per scraping target Article 6 evidence
    Privacy notice published Public page describing your processing Article 14 transparency
    Data minimisation by design Scrape only fields you actually use Article 5(1)(c)
    Purpose limitation Define use cases; do not silently expand Article 5(1)(b)
    Storage limits Set retention windows; auto-delete Article 5(1)(e)
    Right-to-erasure mechanism Public email or form for deletion requests Article 17
    DPO or contact appointed If at scale, appoint a Data Protection Officer Articles 37-39
    Records of processing Article 30 register listing each scraping activity Article 30
    DPIA where high risk Required for large-scale profiling or sensitive data Article 35
    Vendor and processor agreements Article 28 contracts with proxy and storage providers Article 28
    Cross-border transfer mechanism SCCs or adequacy if data leaves EEA Chapter V
    Breach response plan 72-hour notification protocol to supervisory authority Article 33
    Pseudonymisation Strip direct identifiers where not strictly needed Article 25

    A team that ticks every row above is genuinely defensible. A team that ticks fewer than half is one regulator letter away from a problem.

    How EU enforcement shifted in 2024 and 2025

    Two trends matter. First, regulators moved from reactive enforcement (responding to complaints) to proactive sweeps targeting AI training data, news scraping, and B2B contact databases. The Italian Garante, the French CNIL, and the Dutch Autoriteit Persoonsgegevens all opened investigations of scraping operators in 2024 and 2025, several of which closed with seven-figure fines. Second, the line between data controller and data processor in scraping pipelines tightened. Buying scraped datasets from third parties no longer insulates you. If you process the data, you are a controller for that processing, full stop, and your Article 28 contract with the seller does not change that.

    The Meta v. Bright Data ruling in the Israeli court, while not GDPR per se, was widely cited by EU regulators in 2025 because it addressed the same fact pattern: scraping public profiles at scale. The court found that scraping logged-out public data did not violate Meta’s terms (they only bind logged-in users), but the EU regulators were quick to point out that absence of a contract violation does not equal a lawful basis under GDPR. Public availability is not a get-out-of-GDPR card.

    For the deeper US-side analysis of similar fact patterns, see the HiQ Labs v LinkedIn ruling explainer.

    Decision tree for an EU-touching scrape

    Use this before you queue a new target.

    Q1: Does the target site host personal data of EU/EEA residents?
        ├── No  -> GDPR likely not in scope. Document the assessment.
        └── Yes -> Q2
    Q2: Do you have a documented lawful basis (LIA preferred)?
        ├── No  -> Stop. Write the LIA first.
        └── Yes -> Q3
    Q3: Have you minimised fields to only what you need?
        ├── No  -> Trim the schema before launching.
        └── Yes -> Q4
    Q4: Is there a published privacy notice describing this processing?
        ├── No  -> Publish the notice; link it in your contact form.
        └── Yes -> Q5
    Q5: Is data leaving the EEA after processing?
        ├── Yes -> Confirm SCCs or adequacy decision are in place.
        └── No  -> Q6
    Q6: Is the volume or sensitivity high enough to trigger DPIA?
        ├── Yes -> Run the DPIA before launch.
        └── No  -> Proceed; log the assessment in your Article 30 register.
    

    Each branch produces an artefact. That paper trail is what defends you in an investigation.

    Special cases: AI training data, journalism, and B2B

    AI training is the highest-risk scraping use case in 2026. The European AI Act now layers obligations on top of GDPR for any training pipeline that touches personal data. Transparency about training data sources is becoming an audit expectation, not a nice-to-have. If you scrape to train a model, document each source, the lawful basis, the consent state, and the opt-out mechanism. Several large model providers were fined in 2025 for scraping personal data without an LIA.

    Journalism and academic research enjoy a partial exemption under Article 85. Member states implement this differently, so a French journalist and a German academic operate under different practical rules even though both fall under the journalism exemption. The exemption is not a blanket immunity; it requires the processing to be genuinely for journalism or research purposes, not commercial repackaging.

    B2B scraping is the gray zone where most commercial teams live. Contact data of identified individuals at companies (jane.doe@acmecorp.com plus a job title) is personal data, full stop. The fact that it is professional context does not remove personhood. Member states diverge on how strictly this is enforced (Germany strict, UK pragmatic, Spain mid), but the safe assumption for cross-border B2B scrapers is that contact-level processing requires an LIA plus an opt-out path.

    Data subject rights and how to honour them

    Articles 12 to 22 grant data subjects rights that survive scraping. The two that bite scrapers hardest are the right to erasure (Article 17) and the right to object (Article 21).

    You must publish a clear way for any individual to request deletion of their data from your stored corpus. Email is acceptable. A public form is better. The response window is one month, extendable to three months for complex requests. You must verify the request (to prevent malicious deletion) but you cannot use verification as a delay tactic.

    Right to object is broader. Anyone can object to processing based on legitimate interest, and you must stop unless you can demonstrate compelling legitimate grounds that override their interests. In practice, most teams just delete and move on, because litigating each objection is more expensive than the marginal data point.

    For internal policy guidance on how to operationalise these rights, see building an ethics-first scraping policy.

    External references

    For the canonical regulation text and EDPB guidance, the official source is gdpr.eu and the EDPB guidelines library at edpb.europa.eu. For the Meta v. Bright Data ruling text and EU regulator commentary on it, the EDPB published a working note in late 2024 that summarises the cross-border implications.

    Comparison: GDPR vs other major privacy regimes for scrapers

    Regime Personal data definition Public data carve-out Right to erasure Extraterritorial reach
    GDPR (EU) Very broad None Yes (Article 17) Yes
    CCPA (California) Broad, household level Partial (publicly available) Yes (limited) Yes if doing business in CA
    PDPA (Singapore) Identifiable individual Broader carve-out (publicly available) Limited Yes
    DPDP (India) Digital personal data Limited carve-out Yes (correction and erasure) Yes
    LGPD (Brazil) Mirrors GDPR None Yes Yes

    The pattern is clear: GDPR is the strictest, CCPA is the most enforcement-active, and the Asia-Pacific regimes are catching up fast. Build for GDPR and the rest follow.

    FAQ

    Is scraping public data legal under GDPR?
    Public availability does not exempt data from GDPR. If a webpage is publicly readable but contains personal data, processing that data still requires a lawful basis. The European Data Protection Board has been explicit on this.

    Do I need consent to scrape?
    Consent is rarely workable for scraping because you cannot meaningfully obtain it from the data subject before collection. Most teams rely on legitimate interest (Article 6(1)(f)), which requires a documented Legitimate Interest Assessment.

    What if the website’s terms forbid scraping?
    Terms of service are a contract issue, not a GDPR issue. Even if scraping breaches terms, GDPR analysis is independent. You can have a clean GDPR position and still face a contract claim, or vice versa.

    Does GDPR apply if I am outside the EU?
    Yes, if the data subjects are in the EU or EEA. Article 3 extends the regulation extraterritorially. Hosting your servers offshore does not move the data outside scope.

    What is the typical fine range in 2026?
    For administrative fines, the range starts in the low six figures for first-time scraping breaches and reaches into the tens of millions for systematic, large-scale, or AI-training violations. The statutory cap is the higher of EUR 20 million or 4 percent of global annual turnover.

    Extended enforcement analysis 2024-2026

    The pace of GDPR scraping enforcement accelerated sharply between 2024 and 2026. The Italian Garante’s Replika decision (April 2024) and the follow-up OpenAI fine (December 2024, EUR 15 million) both pivoted on the same lever, Article 6 lawful basis combined with Article 5(1)(a) transparency. Neither case turned on whether the data was technically public. Both turned on whether the scraper documented a Legitimate Interest Assessment that survived a balancing test, and whether data subjects had a realistic path to object.

    The Hamburg Commissioner’s 2024 guidance on AI training data explicitly states that publicly accessible does not mean publicly licensable for retraining. The French CNIL’s 2025 sandbox guidance for generative AI training reaches the same conclusion. The Dutch DPA’s August 2025 enforcement note added a third leg, saying scrapers operating under legitimate interest must apply purpose limitation at the chunking and embedding stage, not just at ingest.

    For a scraping operator the practical takeaway is that the LIA must now be a living document. Reviewers will expect to see at minimum the original LIA, an updated LIA every twelve months, evidence of the rights-honouring workflow firing in production, and proof that purpose limitation propagated downstream. A static one-page LIA from 2023 will not survive 2026 supervision.

    Implementation patterns that pass scrutiny

    A compliant 2026 scraping pipeline typically includes seven controls.

    1. A robots.txt and AI-crawler header check at fetch time, with the choice logged.
    2. A purpose tag attached to every record at ingest, propagated through embeddings and downstream tables.
    3. A retention TTL applied at the row level, enforced by a daily sweeper.
    4. A pseudonymisation pass that strips direct identifiers before vectorisation.
    5. A source URL and timestamp stored per record so deletion requests can be honoured by URL.
    6. A data subject request inbox with a measured median response time below seventy two hours.
    7. A quarterly LIA review with a one-page diff against the prior version.

    These controls cost roughly two engineering weeks to set up and a quarter of one engineer ongoing. That is materially cheaper than a single regulator inquiry, which typically consumes four to six engineering weeks of unplanned work.

    Cross-jurisdiction comparison expanded

    Question EU GDPR UK GDPR California CCPA Singapore PDPA India DPDP
    Public data exempt? No No Partial (publicly available carve-out) No (still personal data) No
    Lawful basis required? Yes (six options) Yes (six options) Notice plus opt-out Consent or deemed consent Consent or legitimate uses
    Right to object Yes Yes Right to opt-out of sale or share Withdrawal of consent Yes
    Max fine EUR 20M or 4 percent GBP 17.5M or 4 percent USD 7,500 per intentional violation SGD 1M INR 250 crore
    Cross-border transfer rules SCCs, adequacy UK SCCs, adequacy Limited Transfer limitation obligation Notified countries only

    Additional FAQ

    Does pseudonymisation remove GDPR scope?
    No. Pseudonymous data remains personal data under Article 4(5) because re-identification is possible. Anonymisation in the strict sense (irreversible) does remove scope, but most scraping pipelines do not achieve true anonymisation.

    What about scraping that runs entirely outside the EU?
    Article 3(2) extraterritoriality means the scraper is in scope if it targets EU data subjects or monitors their behaviour. Hosting infrastructure outside the EU does not by itself remove scope.

    Can I rely on contract as a lawful basis instead of legitimate interest?
    Only when a contract with the data subject genuinely requires the scrape. For most third-party scraping there is no contract with the data subject, so Article 6(1)(b) does not apply. Legitimate interest under Article 6(1)(f) is the usual basis.

    What does proportionate mean in the LIA balancing test?
    Proportionate means the scrape collects only what is necessary, runs at a frequency justified by the purpose, applies de-identification where possible, and stops when the purpose is met. Indefinite retention rarely passes the test.

    Practical lawful basis selection

    The choice of GDPR lawful basis sits at the centre of every scraping decision. Six bases are listed in Article 6, but only three are realistically available to a scraping operator. Consent under Article 6(1)(a) requires affirmative action by the data subject, which is rarely obtainable for third-party scraping. Contract under Article 6(1)(b) requires a contract with the data subject, which scrapers typically do not have. Legitimate interest under Article 6(1)(f) is the workhorse, requiring a documented assessment that weighs the legitimate interest of the controller against the rights and freedoms of the data subject.

    Public interest under Article 6(1)(e) is occasionally relevant for journalism, academic research, or specific regulatory functions. Vital interest under Article 6(1)(d) almost never applies to scraping. Legal obligation under Article 6(1)(c) applies when a specific law requires the processing, which is unusual for commercial scraping.

    The Legitimate Interest Assessment is therefore the document that determines whether a scrape is lawful. A 2026-quality LIA includes a stated purpose, an identification of the legitimate interest, a necessity test, a balancing test against data subject rights, a description of safeguards, and a conclusion. Each section should be specific to the scrape, not boilerplate. Regulator inquiries routinely call out boilerplate LIAs as evidence of bad faith.

    What changes when special category data is involved

    Article 9 of the GDPR creates a separate regime for special category data, including racial or ethnic origin, political opinions, religious beliefs, trade union membership, genetic data, biometric data, health data, sex life, and sexual orientation. Special category data may not be processed at all unless one of ten exceptions in Article 9(2) applies.

    For scrapers the practical implication is that scraping that picks up special category data inadvertently still falls under Article 9. Indirect inference (for example name plus location plus organisation suggesting religious affiliation) can also trigger Article 9. The recommended posture is to detect and exclude special category signals at ingest, with a documented filter and a periodic audit.

    The Italian Garante’s 2024 investigation of OpenAI explicitly cited the scraping of special category data as one factor in the eventual fine. The takeaway is that scrapers cannot rely on the data being public to escape Article 9. The lawful basis must come from one of the Article 9(2) exceptions, none of which neatly fits commercial scraping.

    Documenting the right to object

    Article 21 gives data subjects the right to object at any time to processing based on legitimate interest. The controller must stop processing unless it can demonstrate compelling legitimate grounds that override the rights and freedoms of the data subject, or processing is for the establishment, exercise, or defence of legal claims.

    A scraper relying on Article 6(1)(f) must therefore have an objection workflow. The 2026 best practice is a public-facing form that accepts an identifier (name, email, profile URL) and routes the request to a queue with a measured response time. The response should confirm that processing has stopped or explain why a compelling legitimate ground continues to apply. The latter response is rare and should be reviewed by counsel before sending.

    The objection workflow must operate in addition to the Article 17 right to erasure. The two rights are related but distinct. Erasure is a request to delete the data. Objection is a request to stop the processing. A controller may need to respond to both for the same individual.

    Next steps

    The fastest way to move from exposure to compliance is to write the LIA first, publish the privacy notice second, and stand up the right-to-erasure inbox third. Everything else (DPIA, Article 30 register, processor contracts) builds on top of those three. For broader policy and team-rollout guidance, head to the DRT compliance and ethics hub and start with the ethics-first policy guide.

    This guide is informational, not legal advice.

  • How to scrape websites with browser-use in 2026

    How to scrape websites with browser-use in 2026

    browser-use scraping in 2026 is the cleanest way to get an LLM to drive a real Chromium session and pull structured data from sites that punish naive HTTP scrapers. The library wraps Playwright with a reasoning loop powered by GPT-4o, Claude 3.5 Sonnet, or Gemini 1.5 Pro, watches the live DOM, and decides each click, scroll, and form fill on the fly. If you have spent the last six years writing brittle CSS selectors that snap every time a marketing team renames a div, this is the productivity jump you have been waiting for.

    This guide shows you the full pipeline. We install browser-use, wire it to a proxy pool, run it against a JavaScript-heavy target, parse structured output back into a Pydantic model, harden the agent against captchas and bot defenses, and benchmark cost so you do not get surprised by an OpenAI invoice at the end of the month.

    What browser-use actually is

    browser-use is an open-source Python library, MIT licensed, that exposes a single Agent class. You give it a natural language task, a starting URL, an LLM, and an optional list of allowed actions. The library boots a Chromium instance through Playwright, screenshots the page, builds a numbered representation of every interactive element, and asks the LLM what to do next. The LLM returns a JSON action like {"click": 14} or {"input": {"index": 23, "text": "ergonomic keyboard"}}, browser-use executes it, and the loop continues until the agent emits a done action with the extracted payload.

    Two things make this approach better than vanilla Playwright for scraping. First, the agent recovers from layout changes automatically because it sees the page the way a human does, not through brittle selectors. Second, you can instruct it in plain English. A task like “find the highest rated wireless mouse under fifty dollars and return the product URL” works on Amazon, Best Buy, and Lazada with no per-site code.

    The downside is cost and latency. Each step costs an LLM call, and a typical product page takes 6 to 15 steps. We will show how to keep the bill sane in a later section.

    How the agent loop works under the hood

    The internal loop is straightforward enough to read in an afternoon. On each iteration browser-use captures three artifacts: a viewport screenshot, a DOM snapshot reduced to interactive elements, and the URL plus tab list. These are packed into a multimodal prompt with the task description, the action history, and a system prompt that defines the available actions. The LLM returns a JSON object that names exactly one action and any arguments. browser-use validates the action against its registry, executes it through Playwright, waits for network idle plus a configurable settle delay, and starts the next iteration.

    Two design choices shape everything else. The element index is rebuilt every step because the DOM after a click is rarely the DOM before it, so the LLM never references a stale index. And the action registry is open: you can register custom actions like solve_captcha or download_pdf that the LLM can choose alongside the built-in click, type, scroll, and navigate primitives.

    Where it fits in the agentic scraping landscape

    browser-use sits in the middle of three nearby tools. Stagehand from Browserbase is more developer-instructed, with a page.act("click the buy button") style API. OpenAI Operator and Anthropic Computer Use are full computer-control agents that drive a virtual machine, not just a browser. browser-use is the sweet spot when you want full autonomy inside a browser without paying for a managed VM.

    Installing the stack

    Pin everything. browser-use moves fast, the Playwright Chromium build pins matter, and pip resolutions can break if you do not lock your requirements.txt.

    python -m venv .venv
    source .venv/bin/activate
    pip install browser-use==0.2.4 playwright==1.49.0 langchain-openai==0.2.10 pydantic==2.9.2
    playwright install chromium
    

    Set your LLM key:

    export OPENAI_API_KEY="sk-..."
    # or
    export ANTHROPIC_API_KEY="sk-ant-..."
    # or
    export GOOGLE_API_KEY="AI..."
    

    Verify the install with a one-line agent against a forgiving target:

    import asyncio
    from browser_use import Agent
    from langchain_openai import ChatOpenAI
    
    async def main():
        agent = Agent(
            task="Go to example.com and return the H1 text",
            llm=ChatOpenAI(model="gpt-4o-mini"),
        )
        result = await agent.run()
        print(result)
    
    asyncio.run(main())
    

    If this prints “Example Domain” and exits cleanly, you are ready to point the agent at real targets.

    Docker image for reproducible runs

    For CI and production deployment, build an image instead of running pip on the host. The Playwright base image already includes the right Chromium build, fonts, and shared libraries that headless Chromium silently needs.

    FROM mcr.microsoft.com/playwright/python:v1.49.0-jammy
    
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    
    COPY . .
    
    ENV PYTHONUNBUFFERED=1
    ENV BROWSER_USE_HEADLESS=true
    
    CMD ["python", "-m", "scrapers.runner"]
    

    Use this image for every environment, including local dev with docker compose run scraper. The number of “works on my mac” bugs that disappear when everyone runs the same Chromium build is striking.

    A first scraping agent

    Let us scrape Hacker News for the top five stories with score and submitter. This site is friendly to bots, gives us a stable test target, and lets us focus on the agent shape rather than fighting Cloudflare.

    import asyncio
    from typing import List
    from pydantic import BaseModel
    from browser_use import Agent, ActionResult, Controller
    from langchain_openai import ChatOpenAI
    
    class HNStory(BaseModel):
        rank: int
        title: str
        url: str
        score: int
        submitter: str
    
    class HNResult(BaseModel):
        stories: List[HNStory]
    
    controller = Controller(output_model=HNResult)
    
    async def main():
        agent = Agent(
            task=(
                "Visit https://news.ycombinator.com and return the top 5 stories. "
                "For each story include rank, title, link URL, score, and submitter username."
            ),
            llm=ChatOpenAI(model="gpt-4o", temperature=0),
            controller=controller,
            max_failures=3,
        )
        history = await agent.run()
        final = history.final_result()
        parsed = HNResult.model_validate_json(final)
        for s in parsed.stories:
            print(s.rank, s.score, s.title, s.url)
    
    asyncio.run(main())
    

    The Controller with an output_model forces the agent to emit valid JSON matching your Pydantic schema. This is the single most important pattern for production scraping with browser-use because it eliminates the JSON-parsing headaches that plague unstructured agent output.

    A more realistic ecommerce example

    Hacker News is a friendly target. Let us look at something closer to the work most teams actually do, scraping a paginated product listing where the agent has to decide when to stop scrolling and how to follow into a detail page.

    import asyncio
    from typing import List, Optional
    from pydantic import BaseModel, Field
    from browser_use import Agent, Controller
    from langchain_openai import ChatOpenAI
    
    class Product(BaseModel):
        title: str
        price_usd: float
        rating: Optional[float] = None
        review_count: Optional[int] = None
        in_stock: bool = True
        detail_url: str
        primary_image: Optional[str] = None
    
    class ProductPage(BaseModel):
        products: List[Product] = Field(min_length=1, max_length=20)
        next_page_url: Optional[str] = None
    
    controller = Controller(output_model=ProductPage)
    
    async def scrape_listing(start_url: str) -> ProductPage:
        agent = Agent(
            task=(
                f"Visit {start_url}. Scroll until at least 12 product cards are visible "
                "or you see a Load More button (do not click it). Return up to 20 products "
                "with title, price in USD, rating, review count, stock status, detail URL, "
                "and primary image URL. If a clear pagination link to the next page exists, "
                "include its URL."
            ),
            llm=ChatOpenAI(model="gpt-4o", temperature=0),
            controller=controller,
            max_failures=2,
            max_steps=25,
        )
        history = await agent.run()
        return ProductPage.model_validate_json(history.final_result())
    

    The max_steps cap is a hard guardrail. Without it, an agent that misreads the page can loop for a hundred steps and burn a few dollars on a single failed run.

    Adding a custom action

    Sometimes the model wants to do something the default action set does not cover, like waiting for a specific text to appear or downloading a file. Register a custom action and the LLM gains it as an option.

    from browser_use import Controller, ActionResult
    
    @controller.action("Wait for an order confirmation number to appear on screen")
    async def wait_for_order_number(page) -> ActionResult:
        await page.wait_for_selector("text=/Order #\\d+/", timeout=15000)
        text = await page.locator("text=/Order #\\d+/").first.text_content()
        return ActionResult(extracted_content=text, include_in_memory=True)
    

    The string after @controller.action(...) is what the LLM sees in its action menu, so write it like a tool description. Vague names cause the LLM to never pick the action.

    Routing through a proxy pool

    For anything bigger than a personal project, you need proxies. Mobile and residential IPs avoid the data center bans that hit any sustained scraping operation. browser-use accepts standard Playwright proxy config:

    from browser_use import Agent, Browser, BrowserConfig
    
    browser = Browser(
        config=BrowserConfig(
            headless=True,
            proxy={
                "server": "http://proxy.example.com:8000",
                "username": "user-rotate",
                "password": "secret",
            },
        )
    )
    
    agent = Agent(
        task="...",
        llm=ChatOpenAI(model="gpt-4o"),
        browser=browser,
    )
    

    For ASEAN targets where you need a real local IP, Singapore mobile proxy gives you rotating Singtel and StarHub mobile IPs that pass even strict carrier-level checks. For US and EU, Bright Data and Oxylabs both have first-party browser-use compatibility documented.

    Rotate per agent run, not per request. Mid-session IP swaps can trigger TLS resumption errors and confuse session cookies.

    Sticky sessions versus rotating sessions

    There is a real tradeoff in how you bind a session to an IP. Sticky sessions keep the same exit IP for an entire agent run, which is what most ecommerce flows need because cart, checkout, and account pages all rely on a stable session. Rotating sessions assign a fresh IP per request, which is cheaper and good for one-shot listing scrapes but breaks any flow with a multi-page state.

    A pattern that works well in production is to use sticky sessions for the agent run and rotating sessions for any background HTML enrichment workers that hit static product pages.

    Geo-targeted IPs and locale alignment

    If you need a German product price in euros, your IP, your Accept-Language header, and your locale all need to agree. A US IP combined with a de-DE locale is a reliable trigger for cloaking on retailers like Mediamarkt and Otto, and many of them quietly serve a different DOM that breaks selectors a US-only test would pass.

    from browser_use.browser.context import BrowserContextConfig
    
    context_config = BrowserContextConfig(
        locale="de-DE",
        timezone_id="Europe/Berlin",
        extra_http_headers={"Accept-Language": "de-DE,de;q=0.9"},
    )
    

    Handling anti-bot defenses

    Cloudflare Turnstile, DataDome, and PerimeterX are the three you will hit most often in 2026. browser-use plus a clean residential or mobile IP defeats Turnstile in the agent loop because the LLM can solve the visual challenge by clicking the checkbox and waiting for the JavaScript to settle. DataDome is harder. You need realistic mouse movement, which browser-use approximates by adding randomized delays.

    Configure the browser with stealth defaults:

    from browser_use import Browser, BrowserConfig
    from browser_use.browser.context import BrowserContextConfig
    
    context_config = BrowserContextConfig(
        user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
        viewport={"width": 1440, "height": 900},
        locale="en-US",
        timezone_id="America/New_York",
    )
    
    browser = Browser(
        config=BrowserConfig(
            headless=False,  # headed wins more often than headless on bot-defended sites
            chromium_sandbox=False,
            extra_chromium_args=[
                "--disable-blink-features=AutomationControlled",
                "--disable-features=IsolateOrigins,site-per-process",
            ],
        )
    )
    

    For sites that go beyond fingerprinting and require a paid CAPTCHA solver, browser-use can integrate with 2Captcha or CapSolver via a custom action registered through the Controller.

    Empirical bypass rates by defense vendor

    Numbers from a March 2026 internal benchmark across 500 page loads per defense, per setup. The “naive” column is browser-use with default Chromium and a data center IP. “Hardened” is browser-use with the stealth defaults above plus a mobile IP.

    Defense vendor Naive success Hardened success Failure pattern
    Cloudflare Turnstile 34% 92% JS challenge, easy with mobile IP
    DataDome 12% 71% Mouse movement scoring, headed wins
    PerimeterX (HUMAN) 18% 64% Sensor data, needs longer warmup
    Akamai Bot Manager 22% 68% TLS fingerprint heavy, JA4 matters
    Kasada 8% 41% Hardest tier, often needs paid solver
    Imperva 28% 78% Cookie staling, rotate after 50 pages

    Kasada is a wall. If your target uses it, budget for either a paid bypass service or a complete rethink of the scraping approach.

    Mouse movement realism

    Out of the box, browser-use clicks at a coordinate and DataDome scores that as bot-like. A small custom action that traces a curved path from the current mouse position to the target element raises the human-likeness score noticeably.

    import math, random
    
    @controller.action("Click an element with human-like mouse movement")
    async def humanlike_click(page, index: int) -> ActionResult:
        box = await page.locator(f"[data-index='{index}']").bounding_box()
        if not box:
            return ActionResult(error="Element not visible")
        target_x = box["x"] + box["width"] / 2
        target_y = box["y"] + box["height"] / 2
        steps = 25
        for i in range(steps):
            t = i / steps
            x = target_x * t + random.uniform(-2, 2)
            y = target_y * t + math.sin(t * math.pi) * 50
            await page.mouse.move(x, y)
        await page.mouse.click(target_x, target_y)
        return ActionResult(extracted_content="clicked")
    

    Comparing browser-use against vanilla Playwright

    Dimension browser-use Vanilla Playwright
    Time to first scrape 5 minutes 1 to 4 hours
    Per-page cost $0.01 to $0.05 in LLM tokens Near zero infra cost
    Resilience to layout change High, agent re-derives clicks Low, selectors break
    Maintenance burden Update the prompt Rewrite selectors
    Throughput 1 to 5 pages per minute per agent 30 to 100 pages per minute per worker
    Best fit Long-tail sites, exploratory scraping, fast prototypes High-volume known-shape pipelines
    Debuggability Replay history, screenshots per step Standard Playwright trace viewer
    Onboarding new engineer Hours, mostly prompt practice Days, learn the selector and wait dance
    Handling A/B tests Transparent, agent picks the visible variant Each variant needs a code path
    Captcha handling Often solves Turnstile in-loop Needs explicit solver integration

    The honest read in 2026 is that browser-use is the right tool when the scraping target changes often, when you have many sites to support, or when you need to ship in days not weeks. Plain Playwright still wins for the high-volume, known-shape pipelines that most ecommerce monitoring teams run.

    Cost benchmarking with realistic targets

    Cost is the question every engineering manager asks the moment a browser-use proof of concept ships. Token consumption is dominated by the page screenshot captions, not the action JSON.

    Rough per-page numbers from production runs in early 2026:

    LLM Steps per page Input tokens Output tokens Cost per page
    GPT-4o 8 12,000 600 $0.039
    GPT-4o-mini 11 18,000 800 $0.003
    Claude 3.5 Sonnet 7 11,000 500 $0.041
    Claude 3.5 Haiku 12 19,000 700 $0.012
    Gemini 1.5 Pro 9 14,000 700 $0.024
    Gemini 1.5 Flash 13 21,000 800 $0.005
    Llama 3.2 90B Vision (self-hosted) 10 16,000 700 $0.002

    Use GPT-4o-mini for known-good sites where the agent rarely takes a wrong turn. Reserve Sonnet for sites with adversarial layouts. Gemini Pro is the value pick if your target has long pages that benefit from the 2 million token context window.

    For a deeper cost dive, see our AI scraping cost benchmark 2026 which includes a full Lazada and Amazon comparison.

    Three levers that cut per-page cost

    The biggest wins are not LLM swaps. They are loop discipline.

    First, downsample the screenshot. browser-use defaults to the full viewport at full DPR, which on a Retina display is over 5 megapixels. Cropping to the visible content and capping at 1280 wide cuts vision tokens by roughly 40 percent with no measurable accuracy hit on product listings.

    Second, prune the action history. By default the LLM sees every prior step. Past step 5 the marginal benefit drops fast and the input tokens balloon. Capping the history window at 4 prior steps cuts cost on long runs by roughly 30 percent.

    Third, exit early. Define a tight output_model that the agent must produce, and the moment all required fields are populated, an internal hint in the system prompt nudges the agent to emit done. This shaves the average run from 11 to 8 steps on listing pages.

    Storing and validating output

    browser-use returns a History object with the full step trace, screenshots, and final result. Persist these for debugging and replay:

    import json
    from pathlib import Path
    
    history = await agent.run()
    
    run_dir = Path(f"runs/{history.history[0].state.url.split('/')[-1]}")
    run_dir.mkdir(parents=True, exist_ok=True)
    
    (run_dir / "result.json").write_text(history.final_result())
    (run_dir / "trace.json").write_text(json.dumps(history.model_dump(), default=str))
    
    for i, step in enumerate(history.history):
        if step.state.screenshot:
            (run_dir / f"step_{i:03d}.png").write_bytes(step.state.screenshot)
    

    Validate every output against the Pydantic schema before writing to your warehouse. browser-use occasionally returns partial results when the agent times out, and a strict schema catches these at the boundary.

    Schema versioning across releases

    If you persist results long-term, version your Pydantic schemas. browser-use updates and prompt tweaks shift the shape of agent output subtly, and an unversioned warehouse table will accumulate field drift.

    class ProductV2(BaseModel):
        schema_version: Literal["2.0"] = "2.0"
        title: str
        price_usd: float
        currency_original: str = "USD"
        rating: Optional[float] = None
    

    When you change the model, bump the version and write a migration in the same commit. Future-you will never regret this.

    Production patterns

    Three patterns matter when you take browser-use beyond a notebook.

    First, run agents under a worker pool with a hard wall-clock timeout. The agent loop can spin if the LLM gets confused, and a 30-second cap per task with a retry budget keeps cost predictable.

    Second, cache LLM responses on identical screenshots. browser-use ships a screenshot hash that you can use as a cache key. For sites with stable layouts, this can cut LLM cost in half during regression runs.

    Third, separate the navigation agent from the extraction step. Use browser-use only to reach the target page, then dump the HTML and pass it to a cheaper structured extraction model. This is the pattern documented in our LLM extraction patterns guide and it is the single biggest cost lever once you cross a few thousand pages per day.

    For the official roadmap and feature additions, the browser-use GitHub README is updated with every release and is the canonical reference.

    Concurrency and the rate limit ceiling

    The single most common production scaling mistake is to spin up 100 browser-use workers and watch the OpenAI account hit a tier-2 rate limit at 14,000 tokens per minute. browser-use is token-heavy because of the screenshots, and a single worker easily burns 80,000 tokens per minute on a hot loop.

    A safer pattern is one worker per 50,000 tokens-per-minute of headroom, plus an exponential backoff wrapper around the LLM call that catches 429s and re-queues the step. Combine that with a token-bucket rate limiter on the worker pool itself and the system stays stable under load.

    Production gotchas you only learn the hard way

    • The Chromium sandbox conflicts with some Docker base images. Set chromium_sandbox=False and you avoid an opaque crash on container start.
    • Headless Chromium emits a different navigator.platform than headed, and a few sites use this as a quick bot signal. Override with --user-agent-extra if you must run headless.
    • Long-running browser instances leak memory after roughly 200 pages. Recycle the browser every 100 pages to stay flat.
    • The agent occasionally hallucinates an element index that was just removed by a click. Wrap each action in a try/except that asks the LLM to re-observe on ElementNotFoundError.
    • Cookie banners are the single most common reason a run stalls. Hardcode a “if a cookie banner is visible, accept it” instruction in your task and watch step counts drop.

    When not to use browser-use

    If you are scraping a public API, do not use browser-use. If you are scraping a flat HTML site with stable selectors and you have a working Scrapy project, do not migrate. If you are running ten million pages a month and your unit economics depend on staying under a fraction of a cent per page, browser-use will burn money.

    The right targets are sites with heavy JavaScript, sites that change often, sites with anti-bot defenses that defeat headless Playwright, and the long tail of small targets where writing custom selectors is not worth the engineering hours.

    Frequently asked questions

    How does browser-use compare to Stagehand?
    Stagehand is closer in spirit, but Stagehand puts more weight on developer-written instructions per action while browser-use leans on full autonomy. For a side-by-side, see our Stagehand vs Playwright AI scraping comparison.

    Does browser-use work with local LLMs?
    Yes. Anything that speaks the OpenAI API works, including Ollama, vLLM, and LM Studio. The vision capabilities of the local model dominate quality. Llama 3.2 90B Vision and Qwen 2.5 VL 72B are the strongest open-source picks in early 2026.

    Can I run browser-use in serverless environments?
    Cloudflare Workers, no. Standard Lambda, only with the Chromium layer and significant cold-start tuning. The cleanest production target is a long-running container on Fargate or a small VPS with a worker queue.

    What about session cookies and login state?
    Pass storage_state to the Browser config to load a cookies-and-localStorage snapshot. Generate the snapshot once with a manual login, store it encrypted, reuse across runs.

    How do I debug a stuck agent?
    Set headless=False, run with Agent(..., generate_gif=True), and watch the GIF after the run. The visual replay tells you exactly which step the agent misread.

    Can I run multiple browser-use agents in parallel inside one process?
    Yes, the library is async-safe. The practical limit is roughly one agent per CPU core because Chromium itself is the bottleneck, and each instance holds 200 to 400 MB of RAM. For more parallelism, distribute across processes or hosts.

    How do I handle infinite scroll pages?
    Add an explicit instruction in your task: “Scroll until you see at least N items or until 3 consecutive scrolls reveal no new content, then stop.” Without an exit condition the agent will scroll until step budget exhaustion.

    What happens when the LLM provider is rate-limited?
    browser-use surfaces the LLM exception. Wrap agent.run() in a tenacity retry decorator with exponential backoff, and queue runs through Redis or SQS so a transient outage does not lose work.

    If you want to compare browser-use to its closest competitors before committing, browse our AI modern scraping category for head-to-head reviews of Stagehand, Browserbase, Scrapybara, and OpenAI Operator.

  • How to Scrape Etsy Best Sellers and Trending Tags (2026)

    Etsy surfaces its best-seller badges and trending tag labels on public product pages, and scraping them at scale is genuinely useful for competitive research, niche validation, and dropshipping product discovery. The catch is that Etsy runs aggressive bot detection, rate-limits unauthenticated crawlers hard, and returns different HTML depending on whether your request looks like a browser or a script. Here is a practical 2026 guide to getting the data reliably.

    What Data You Can Actually Pull

    Etsy does not expose a public API for best-seller or trending data. Everything you care about lives in rendered HTML or embedded JSON-LD on product and search pages.

    Useful fields per listing:

    • Listing title, price, sale price
    • “Bestseller” badge (a with class wt-badge--small)
    • Star rating and review count
    • Shop name and sales count
    • Tags (visible on listing pages, not search results)
    • Estimated monthly sales (inferred from review velocity, not served directly)

    Trending tags appear in Etsy’s search autocomplete (/api/v3/ajax/typeahead/etsy/term) and in the “Shop by popular tags” carousels on category pages. Both endpoints are accessible without login but require consistent headers.

    How Etsy Detects Bots

    Before writing a single line of code, understand the detection stack you are up against:

    Layer Method Notes
    TLS fingerprinting JA3/JA4 hash check Requests/httpx fail without spoofing
    Header validation User-Agent, Accept, Sec-Fetch-* Missing Sec-Fetch headers = instant block
    IP reputation DataDome (embedded on most pages) Datacenter IPs blocked by default
    Behavioral analysis Mouse events, scroll timing Only triggers on JS-heavy category pages
    CAPTCHA hCaptcha Triggered on rapid listing traversal

    The TLS fingerprint check is the highest-priority hurdle. Plain requests with a spoofed User-Agent still fails because the TLS handshake looks like Python. Use curl_cffi with impersonate="chrome120" or route through a residential proxy with its own TLS termination.

    DataDome is the persistent layer. It tracks request cadence across sessions and will silently serve degraded HTML (no badge data, no review count) long before it serves a hard block. This is similar to the detection stack you encounter when doing more general marketplace work like scraping Walmart Marketplace seller data.

    Scraping Best-Seller Listings: Working Approach

    For listing-level data, the most reliable path in 2026 is:

    1. Build a seed URL list from Etsy search (/search?q=&explicit=1&ship_to=US)
    2. Paginate through results pages (up to page 250, ~6000 results per query)
    3. For each listing URL, fetch the product page and parse the embedded