     [Blog](https://scrapfly.io/blog)   /  [cloud-browser](https://scrapfly.io/blog/tag/cloud-browser)   /  [Camoufox for Web Scraping: Setup, Proxies and Servers](https://scrapfly.io/blog/posts/camoufox-for-web-scraping)   # Camoufox for Web Scraping: Setup, Proxies and Servers

 by [Hisham Medhat](https://scrapfly.io/blog/author/hisham) Nov 09, 2026 35 min read [\#cloud-browser](https://scrapfly.io/blog/tag/cloud-browser) [\#headless-browser](https://scrapfly.io/blog/tag/headless-browser) [\#proxies](https://scrapfly.io/blog/tag/proxies) 

 [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fcamoufox-for-web-scraping "Share on LinkedIn") [  ](https://x.com/intent/tweet?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fcamoufox-for-web-scraping&text=Camoufox%20for%20Web%20Scraping%3A%20Setup%2C%20Proxies%20and%20Servers "Share on X") [  ](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fcamoufox-for-web-scraping "Share on Facebook")    

 

 

Summarize this article with

 [  ](https://chat.openai.com/?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fcamoufox-for-web-scraping) [  ](https://claude.ai/new?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fcamoufox-for-web-scraping) [  ](https://x.com/i/grok?text=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fcamoufox-for-web-scraping) [  ](https://www.perplexity.ai/search/new?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fcamoufox-for-web-scraping) [  ](https://www.google.com/search?udm=50&aep=11&q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fcamoufox-for-web-scraping) 



         

  **Browser API**Automate browsers in the cloud with full JavaScript rendering support.

 

 [ Learn More  ](https://scrapfly.io/products/cloud-browser-api) [  Docs ](https://scrapfly.io/docs/scrape-api/javascript-rendering) 

 

 

Camoufox is a Firefox build made for web scraping, so we took it for a spin with a small scraper on our Linux machine. Right after the product titles, it printed what the page sees of our browser:

text

```text
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:156.0) Gecko/20100101 Firefox/156.0
webdriver: False
timezone: Africa/Cairo
```



On the first line we see a Windows Firefox, and the second line shows no automation flag - where did our Linux machine go? All of it came from just a few lines of Playwright code!

The last line tells another story. `Africa/Cairo` is our own machine's clock, so behind a US proxy the page would get a Cairo clock with a US IP, and that mismatch alone gives our scraper away.

So the browser is the easy part, and the hard part is everything around it: proxies, GeoIP, fingerprints and servers. We'll be setting each of those up using Camoufox 0.5.8 with browser build 156.0.1-beta.36. We'll also take a look at where it still breaks, and when a remote browser is the better fit.



## Key Takeaways

- `camoufox[geoip]` needs Python 3.10+, and `python -m camoufox fetch` then pulls a browser that takes 2.4 GB of disk.
- With `geoip=True`, a proxy's exit IP sets the locale, timezone and WebRTC IP.
- Camoufox looks the exit IP up once at launch, so it's one sticky session per browser.
- Every `NewContext()` context gets its own fingerprint and proxy.
- `launch_server()` shares one fingerprint over an unauthenticated WebSocket.
- The same Playwright page code runs on Scrapfly Cloud Browser once the connect line changes.

**Get web scraping tips in your inbox**Trusted by 100K+ developers and 30K+ enterprises. Unsubscribe anytime.







## What Is Camoufox and How Does It Hide Playwright Automation?

Camoufox is an open-source fork of Firefox for web scraping and browser agents. It comes with a Python library that speaks Playwright's API. Note that the browser code is licensed under MPL-2.0, while the Python package is MIT.

When it comes to fingerprints, Camoufox doesn't inject javascript into the page. That Windows user agent from our first run was set inside Firefox itself, below the page's own scripts. Note that the same goes for the navigator, screen, WebGL, fonts, WebRTC, timezone and locale values listed on the [Camoufox fingerprint page](https://camoufox.com/fingerprint/). To the page, they all look native.

We've covered how sites read these values in our [browser fingerprinting guide](https://scrapfly.io/blog/posts/how-browser-fingerprinting-works).

Remember the `webdriver: False` from our first run? Playwright was driving that browser the whole time, just not over Chrome's CDP. With Firefox, Playwright goes through Juggler - Firefox's own automation protocol.

This is where Camoufox comes in handy - it patches Juggler so our Playwright code works on an isolated copy of the page, which the page's own scripts can't see. Chrome is another matter, and we've covered [how CDP automation gets detected](https://scrapfly.io/blog/posts/chrome-cdp-stealth-browser-automation-detection-explained) in a separate article.

Let's take a look at what the page gets from two Camoufox launches:

python

```python
from camoufox.sync_api import Camoufox

check = "[navigator.platform, `${screen.width}x${screen.height}`, navigator.hardwareConcurrency]"
native = "Object.getOwnPropertyDescriptor(Navigator.prototype, 'platform').get.toString()"

for _ in range(2):
    with Camoufox(headless=True) as browser:
        page = browser.new_page()
        page.goto("https://web-scraping.dev/products")
        print(page.evaluate(check))
        print(page.evaluate(native))
```



Here, we read the platform, screen size and core count in two separate launches and print the source of the `platform` getter. Running it prints:

text

```text
['MacIntel', '1920x1080', 12]
function platform() {
    [native code]
}
['MacIntel', '2560x1440', 12]
function platform() {
    [native code]
}
```



As we can see, our Linux machine shows up as a Mac, and the getter still reads as `[native code]` since the value is set in C++.

Our screen went from 1920x1080 to 2560x1440 between the two launches, as each launch picks a device profile weighted by real-world traffic. We launched it 20 more times and got 14 Windows, 3 macOS and 3 Linux profiles. That's 14 out of 20 on Windows, which is roughly how real visitors are split too. Say a site usually gets 5% of its visitors on Linux, then a day where 20% of them are on Linux would be suspicious on its own.

The core count stayed at our machine's 12 though. That's because a `Camoufox()` launch in 0.5.8 reports the host's count unless `pin_cpu_cores=True` is passed.

Up to version 0.5.6 these profiles came from BrowserForge. From 0.5.7 on it's fpgen, and its `model-2/2026` model comes down with `camoufox fetch`.

That being said, Camoufox wasn't always maintained. Its own [2026 status note](https://camoufox.com/stealth/) admits "a year gap in maintenance due to a personal situation", and that gap cost performance and surfaced new fingerprint inconsistencies.

Releases 0.5.7 and 0.5.8 shipped on October 4 and 6, 2026, so development is active again. Though, the README still warns that "It may not be suitable for stable production use."

[Best Stealth Browsers for Web Scraping in 2026A neutral, benchmark-literate ranking of the best open-source stealth browsers for web scraping in 2026, with the success, speed, and maintenance tradeoffs.](https://scrapfly.io/blog/posts/best-stealth-browsers)



## How Do You Install Camoufox in Python?

Camoufox installs in two steps - the Python package with its `geoip` extra, and then the browser binary. Note that version 0.5.8 requires Python 3.10 or newer:

shell

```shell
pip install -U "camoufox[geoip]==0.5.8"
python -m camoufox fetch
```



The first command installs camoufox 0.5.8 along with Playwright 1.62 and fpgen 1.3. The browser comes from the second one, and on our Linux machine its zip was about 1.3 GB! The fetch took 8 minutes 29 seconds and left a 2.4 GB browser directory behind.

The `geoip` extra also brings a 115 MB IP database along. It's called "GeoIP AIO" and it replaced the MaxMind GeoLite2 file as the default. The fetch also installs a 17 MB uBlock Origin addon, which loads by default.

Unfortunately, Python 3.9 won't stop us with an error. There, `pip install camoufox` quietly resolves to 0.4.11 from January 2025. Meaning no `NewContext()` and none of the 2026 fingerprint and timezone fixes!

Since Camoufox keeps several browser builds side by side, an upgrade could change the behavior under us. So, let's pin the build we tested:

shell

```shell
python -m camoufox set official/stable/156.0.1-beta.36
python -m camoufox active
```



Which prints our pin and the active build:

text

```text
Pinned: official/stable/156.0.1-beta.36 (installed)
official/stable/156.0.1-beta.36 (72637885)
```



Let's check everything that got installed with the `version` command, which prints a status table:

shell

```shell
python -m camoufox version
```



Here's the part that matters, with the paths, hashes and sync times trimmed out:

text

```text
  Camoufox                    v0.5.8
  fpgen                       v1.3.0
  Playwright                  v1.62.0
Browser
  Active                      official/stable/156.0.1-beta.36
  Current browser             v156.0.1-beta.36
  Build date                  Oct 6
  Latest in official/stable?  Yes
GeoIP
  Database                    GeoIP AIO by daijro
Storage
  Browser(s) directory size   2.4 GB
  GeoIP database size         114.9 MB
```



Just like that, the `Active` row matches our pin! The storage rows show 2.4 GB for the browser directory and 114.9 MB for the GeoIP database.

The CLI also has `list`, `remove` and `test` commands, which manage the installed builds. For Playwright basics, see our [Playwright with Python guide](https://scrapfly.io/blog/posts/web-scraping-with-playwright-and-python).



## How Do You Scrape a Page With Camoufox?

We drive Camoufox through Playwright's API, so a Camoufox scraper looks just like a Playwright one with a different launch line. We'll start with a single sync scraper and then scale it up with async workers.

### Run a First Scraper With the Camoufox Sync API

For our first scraper, let's reuse the code from our intro. It loads the product listing on `web-scraping.dev` and prints the product titles, plus a few values the page can see about our browser:

python

```python
from camoufox.sync_api import Camoufox

with Camoufox(headless=True, os="windows") as browser:
    page = browser.new_page()
    page.goto("https://web-scraping.dev/products")
    titles = page.locator(".product h3 a").all_inner_texts()
    print(titles)
    print(page.evaluate("navigator.userAgent"))
    print("webdriver:", page.evaluate("navigator.webdriver"))
    print("timezone:", page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone"))
```



Note that the `Camoufox(headless=True, os="windows")` context manager is the only Camoufox part in there. Everything inside it is regular Playwright code - an ordinary locator picks up the titles, while the last three lines ask the page what it sees. Our run printed:

text

```text
['Box of Chocolate Candy', 'Dark Red Energy Potion', 'Teal Energy Potion', 'Red Energy Potion', 'Blue Energy Potion']
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:156.0) Gecko/20100101 Firefox/156.0
webdriver: False
timezone: Africa/Cairo
```



As we can see, all five product titles came back. [The user agent](https://scrapfly.io/blog/posts/user-agent-header-in-web-scraping) says Windows NT 10.0 with Firefox 156 even though our scraper ran on Linux, and `navigator.webdriver` is `False` too!

Only the last line is ours. `Africa/Cairo` is the host machine's own clock, as Camoufox leaves the timezone alone when there's no proxy or GeoIP.

That's alright on our own connection, where a Cairo clock next to a Cairo IP looks fine. Behind a proxy in another country though, the same `Africa/Cairo` would be a leak.

We only passed `headless` and `os` to our scraper, while the [Camoufox usage docs](https://camoufox.com/python/usage/) list plenty more launch options. For example, `humanize` moves the cursor like a person would, `locale` sets the language and `persistent_context` keeps a profile between runs.

### Scale Camoufox With the Async API and One Browser per Worker

Our sync scraper goes through one page at a time, which is fine for a handful of URLs. For more of them, let's switch to `AsyncCamoufox`, put the URLs in a queue and split it between workers that each launch their own browser:

python

```python
import asyncio
import time

from camoufox.async_api import AsyncCamoufox

URLS = [f"https://web-scraping.dev/products?page={n}" for n in range(1, 6)]
WORKERS = 3


async def worker(queue, results, failures):
    async with AsyncCamoufox(headless=True) as browser:
        page = await browser.new_page()
        while True:
            url = await queue.get()
            try:
                await page.goto(url, timeout=20_000)
                results[url] = await page.locator(".product h3 a").all_inner_texts()
            except Exception as error:
                failures[url] = repr(error)
            finally:
                queue.task_done()


async def main():
    start = time.perf_counter()
    queue = asyncio.Queue()
    for url in URLS:
        queue.put_nowait(url)
    results, failures = {}, {}
    workers = [asyncio.create_task(worker(queue, results, failures)) for _ in range(WORKERS)]
    await queue.join()
    for task in workers:
        task.cancel()
    await asyncio.gather(*workers, return_exceptions=True)
    products = sum(len(titles) for titles in results.values())
    print(f"{products} products from {len(results)}/{len(URLS)} pages in {time.perf_counter() - start:.1f}s with {WORKERS} browsers")
    print("failures:", failures)


asyncio.run(main())
```



In the example above, `WORKERS` starts three tasks to share the five listing pages in `URLS`. Each `worker()` launches its own `AsyncCamoufox` browser with a single page and keeps taking URLs off the `asyncio.Queue` until it's empty.

A failed page won't stop a worker though. If a page hits the 20 second `timeout`, for example, the `except` block puts the error in `failures` and the worker moves on to the next URL. With `task_done()` in `finally`, `queue.join()` never ends up waiting on a failed page.

Just like that, our three browsers got through the whole queue:

text

```text
25 products from 5/5 pages in 15.9s with 3 browsers
failures: {}
```



That's 25 products from 5/5 pages in 15.9 seconds, and the `failures` dictionary stayed empty. Two more runs took 16.5 and 20.4 seconds, with browser launches included.

So, why one browser per worker and not one page per worker?

Unfortunately, opening pages and contexts [in parallel](https://scrapfly.io/blog/posts/concurrency-vs-parallelism) inside one Camoufox browser is an open bug ([issue #553](https://github.com/daijro/camoufox/issues/553)). So is a Juggler pipe that stops responding ([issue #719](https://github.com/daijro/camoufox/issues/719)). Separate browsers sidestep both of them. Meaning every worker gets its own fingerprint as a bonus!



## How Do You Use Proxies and GeoIP With Camoufox?

Proxies in Camoufox use the same dictionary as Playwright. When it comes to [blocking](https://scrapfly.io/blog/posts/how-to-avoid-web-scraping-blocking-ip-addresses) though, the page plays spot the difference - an IP from one country with a language and clock from another stands out. `geoip=True` builds the locale, timezone and WebRTC IP from the proxy's exit IP, so all three agree.

### Pass a Playwright Proxy Dictionary With geoip=True

Let's take a look at a proxy run that reads the credentials from environment variables and asks the page for its IP, language and timezone:

python

```python
import json
import os

from camoufox.sync_api import Camoufox

proxy = {
    "server": os.environ["PROXY_SERVER"],
    "username": os.environ["PROXY_USERNAME"],
    "password": os.environ["PROXY_PASSWORD"],
}

with Camoufox(headless=True, proxy=proxy, geoip=True) as browser:
    page = browser.new_page()
    page.goto("https://httpbin.dev/anything")
    print("exit IP:", json.loads(page.inner_text("body"))["origin"])
    print("language:", page.evaluate("navigator.language"))
    print("timezone:", page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone"))
```



The `server`, `username` and `password` keys are the ones Playwright already uses. For this run the proxy was a local authenticated HTTP proxy exiting through our own connection in Egypt, so we redacted the exit IP:

text

```text
exit IP: <our exit IP>
language: ar-EG
timezone: Africa/Cairo
```



As we can see, the page got `ar-EG` and `Africa/Cairo`, which both match an Egyptian IP - so the leak from our intro is plugged! Behind a US proxy, we'd expect a US timezone and a locale from one of the languages spoken there instead.

Note that the [GeoIP docs](https://camoufox.com/python/geoip/) recommend [residential proxies](https://scrapfly.io/blog/posts/top-5-residential-proxy-providers) for the best results, since datacenter ranges are easy to flag.

If we drop `geoip=True`, Camoufox prints a warning, shown here without its file and line prefix:

text

```text
LeakWarning: When using a proxy, it is heavily recommended that you pass `geoip=True`.
```



What about SOCKS5? An authenticated SOCKS5 proxy fails at launch with `Browser does not support socks5 proxy authentication`. So, for authenticated proxies, we stick to HTTP.

### Does GeoIP Set the Page Timezone Too?

Yes, the timezone comes from the same lookup as the locale. That being said, [issue #222](https://github.com/daijro/camoufox/issues/222) reported a timezone mismatch under proxies, and it was closed in July 2026 pending a retest after several timezone fixes. So, let's check it without a proxy by passing an IP address straight to `geoip`:

python

```python
import time
from camoufox.sync_api import Camoufox
from camoufox.geolocation import get_geolocation

geo = get_geolocation("8.8.8.8")
print("lookup:", geo.timezone, geo.locale)

start = time.perf_counter()
with Camoufox(headless=True, geoip="8.8.8.8") as browser:
    print(f"launch: {time.perf_counter() - start:.1f}s")
    page = browser.new_page()
    page.goto("https://web-scraping.dev/products")
    print("language:", page.evaluate("navigator.language"))
    print("timezone:", page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone"))
    print("offset:", page.evaluate("new Date().getTimezoneOffset()"))
```



Above, we first use `get_geolocation()` to ask the bundled database where `8.8.8.8` is located. Then we launch a browser with `geoip="8.8.8.8"` so it claims that location and read the clock from inside the page. We ran this on our machine in Cairo and got:

text

```text
lookup: America/Los_Angeles Locale(language='en', region='US', script='Latn')
launch: 1.9s
language: en-US
timezone: America/Los_Angeles
offset: 420
```



The lookup and the page both say `America/Los_Angeles` and `getTimezoneOffset()` gives us `420` which is Los Angeles time and not Cairo's. Note that a timezone regression wouldn't raise any errors so it's a good idea to keep these three `page.evaluate()` lines in our scraper as a canary.

### Should Camoufox Use Sticky or Rotating Proxy Sessions?

Earlier our `ar-EG` and `Africa/Cairo` values came from a single lookup Camoufox does on launch. We can see it in our proxy's log as one `CONNECT api.ipify.org:443` request, right after the browser starts. The answer to that request then sets the locale, timezone and WebRTC IP.

So what happens when we launch a second browser through the same proxy? Let's launch three browsers in a row - two with the same proxy string and one with `localhost` instead of `127.0.0.1`:

python

```python
import os
import time

from camoufox.sync_api import Camoufox

user, password = os.environ["PROXY_USERNAME"], os.environ["PROXY_PASSWORD"]

for server in ["http://127.0.0.1:8899", "http://127.0.0.1:8899", "http://localhost:8899"]:
    proxy = {"server": server, "username": user, "password": password}
    start = time.perf_counter()
    with Camoufox(headless=True, proxy=proxy, geoip=True) as browser:
        print(f"{server}: launched in {time.perf_counter() - start:.2f}s")
```



Here's what our run printed:

text

```text
http://127.0.0.1:8899: launched in 1.89s
http://127.0.0.1:8899: launched in 0.93s
http://localhost:8899: launched in 3.60s
```



Interestingly, the second launch took only 0.93 seconds which is half of the first one. If we look at our proxy's log we can see why: there are only two `api.ipify.org` lookups for our three browsers. In other words, Camoufox caches the lookup for each proxy string for as long as the Python process runs. Only the `localhost` browser got a fresh lookup even though it's the exact same proxy.

This is a problem with rotating proxy gateways that give out a new IP for each request. Every browser launched through the same gateway string would claim the city of the first IP while its pages are loaded from somewhere else.

For this reason it's best to use sticky proxy sessions with Camoufox - one session per browser and a new browser launch whenever we need a new IP. With a new session ID in the proxy username each browser gets its own proxy string and with it its own lookup. We still rotate proxies this way, just between browsers rather than requests, and our [proxy rotation guide](https://scrapfly.io/blog/posts/how-to-rotate-proxies-in-web-scraping) covers how to build such a pool.

The lookup can also break on its own. For example, in [GitHub discussion #397](https://github.com/daijro/camoufox/discussions/397) proxies that work fine in other HTTP clients raised `InvalidProxy` in Camoufox because the lookup needs HTTPS tunneling through the proxy. If we already know our exit IP we can skip the lookup entirely by passing `geoip="<exit IP>"` just like we did with `geoip="8.8.8.8"` in our timezone check.

### Give Each Browser Context Its Own Proxy With NewContext

Camoufox also comes with a `NewContext()` function which creates a browser context with its own fingerprint and its own proxy. This means one browser can hold several identities at once:

python

```python
import os

from camoufox.sync_api import Camoufox, NewContext

proxy = {
    "server": os.environ["PROXY_SERVER"],
    "username": os.environ["PROXY_USERNAME"],
    "password": os.environ["PROXY_PASSWORD"],
}
check = "[navigator.userAgent, `${screen.width}x${screen.height}`, navigator.hardwareConcurrency, Intl.DateTimeFormat().resolvedOptions().timeZone]"

with Camoufox(headless=True) as browser:
    for _ in range(3):
        context = NewContext(browser, proxy=proxy, os="windows")
        page = context.new_page()
        page.goto("https://web-scraping.dev/products")
        print(page.evaluate(check))
        context.close()
```



Above, we call `NewContext(browser, proxy=proxy, os="windows")` three times and for each context `check` reads the user agent, screen size, core count and timezone before we close it:

text

```text
['Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:156.0) Gecko/20100101 Firefox/156.0', '3638x1919', 12, 'Africa/Cairo']
['Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:156.0) Gecko/20100101 Firefox/156.0', '1920x1080', 4, 'Africa/Cairo']
['Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:156.0) Gecko/20100101 Firefox/156.0', '1536x864', 16, 'Africa/Cairo']
```



We got three different machines from three contexts: `3638x1919` with 12 cores, `1920x1080` with 4 and `1536x864` with 16. The user agent is the only thing that repeats as every context really runs on the same Firefox 156 engine.

All three rows also say `Africa/Cairo` which is correct for our proxy. That's because `NewContext()` does its own lookup too - our proxy's log has three `ip-api.com` requests for the exit IP and its timezone, one for each context.

Plain `browser.new_context()` on the other hand doesn't do any of that. When we called it twice the new contexts kept the browser's own Linux user agent, `1920x1080` screen and 12 cores.

Finally, let's see what happens with a broken proxy. We gave our proxy a wrong password, it replied with `407` and logged a failed authentication and Camoufox 0.5.8 raised this error instead of falling back to our host's values:

text

```text
InvalidIP Could not look up the proxy's exit IP and timezone. Pass webrtc_ip and timezone_id explicitly to skip the lookup: 407 Client Error: ...
```



This is actually good news as a broken proxy can't leave our context quietly running on our real Cairo clock. Either the lookup succeeds or we follow the error's advice and pass `webrtc_ip` and `timezone_id` ourselves.



Scrapfly

#### Need a cloud browser for scraping?

Run headless browsers at scale with Scrapfly Cloud Browser — no infrastructure to manage.

[Try Free →](https://scrapfly.io/register)## How Do You Run Camoufox Headless on a Server?

On a Linux server Camoufox can run with `headless=True` or inside a virtual display with `headless="virtual"`. It can also run as a remote Playwright server, which other machines connect to over a WebSocket. For a refresher on the terms, see [what a headless browser is](https://scrapfly.io/blog/posts/what-is-a-headless-browser).

### headless=True vs headless="virtual" (Xvfb) on Linux

Every Camoufox launch in this article so far passed `headless=True`, and both modes work. The [virtual display docs](https://camoufox.com/python/virtual-display/) warn that headless mode "may still be detectable in the future" and recommend a virtual display instead.

The virtual display comes from Xvfb, which on Debian-based systems is one `sudo apt-get install xvfb` away. Then we pass `"virtual"` as our `headless` value:

python

```python
from camoufox.sync_api import Camoufox

with Camoufox(headless="virtual") as browser:
    page = browser.new_page()
    page.goto("https://web-scraping.dev/products")
    print(page.locator(".product h3 a").all_inner_texts()[:2])
```



Here, Camoufox starts its own Xvfb display in the background and runs a headed browser inside it. Let's take a look at what our run printed:

text

```text
['Box of Chocolate Candy', 'Dark Red Energy Potion']
```



As we can see, these are the same first two titles as our first scraper - only this time the browser ran headed, on a display nobody sees!

### Run Camoufox as a Remote Playwright Server

Camoufox also comes with a `launch_server()` function which turns the browser into a Playwright WebSocket server. This means any Playwright client can connect to it and drive it remotely:

python

```python
from camoufox.server import launch_server

launch_server(headless=True, port=9333, ws_path="camoufox")
```



When we run it the server stays up and prints its endpoint:

text

```text
Launching server...
Server launched: 2.146s
Websocket endpoint: ws://[::1]:9333/camoufox
```



The server took 2.146 seconds to start and now it simply waits for clients to connect. Our scrapers can then connect to it instead of each launching their own browser, so the browser itself can stay up on a bigger machine.

By default though, Camoufox binds the server to `[::1]` which is the IPv6 loopback address. Meaning for now only programs running on the same machine can reach it.

Our client script doesn't import Camoufox at all, as Playwright's Firefox `connect()` method is all it needs to reach the endpoint. Let's open two browser contexts one after another and see what each of them reports:

python

```python
import time

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    start = time.perf_counter()
    browser = p.firefox.connect("ws://localhost:9333/camoufox")
    print(f"connected in {time.perf_counter() - start:.2f}s to {browser.version}")
    for _ in range(2):
        context = browser.new_context()
        page = context.new_page()
        page.goto("https://web-scraping.dev/products")
        print(page.locator(".product h3 a").all_inner_texts()[:2])
        print(page.evaluate("[navigator.userAgent, `${screen.width}x${screen.height}`]"))
        context.close()
    browser.close()
```



Here's what our client printed:

text

```text
connected in 0.09s to 156.0.1-beta.36
['Box of Chocolate Candy', 'Dark Red Energy Potion']
['Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:156.0) Gecko/20100101 Firefox/156.0', '1920x1080']
['Box of Chocolate Candy', 'Dark Red Energy Potion']
['Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:156.0) Gecko/20100101 Firefox/156.0', '1920x1080']
```



As we can see, the client connected in 0.09 seconds to build 156.0.1-beta.36 and scraped the same two products in both contexts. That being said, both contexts also came back with the same Ubuntu user agent and the same `1920x1080` screen.

This is expected, as the [remote server docs](https://camoufox.com/python/remote-server/) put it: "Because servers only use one browser instance, fingerprints will not rotate between sessions." In other words, if we want a new fingerprint we have to restart the server.

What about cookies and logins? Normally we'd keep those in a persistent profile with `persistent_context` and `user_data_dir` so let's try passing the same two to `launch_server()`:

python

```python
from camoufox.server import launch_server

launch_server(headless=True, port=9335, ws_path="camoufox", persistent_context=True, user_data_dir="profile")
```



Here's what we got back:

text

```text
ValueError: launch_server() does not support 'persistent_context': Playwright cannot serve a persistent context over a websocket endpoint. Use Camoufox(persistent_context=True, ...) in-process instead.
```



If we look at the error we can see why: Playwright can't serve a persistent context over a websocket. In other words, persistent profiles are only there when Camoufox runs in-process. Mixing Playwright versions doesn't work either as `connect()` needs the client's major and minor version to match the server's, which for us was 1.62.0.

Next, what if our scrapers run on other machines? For this we can pass `host="0.0.0.0"` to `launch_server()`:

python

```python
from camoufox.server import launch_server

launch_server(headless=True, port=9334, ws_path="camoufox", host="0.0.0.0")
```



This time the server took 3.602 seconds to start and printed a new endpoint:

text

```text
Launching server...
Server launched: 3.602s
Websocket endpoint: ws://0.0.0.0:9334/camoufox
```



Interestingly, when we pointed a client like the one above at our Tailscale address it scraped the first product without any credentials:

text

```text
156.0.1-beta.36 ['Box of Chocolate Candy']
```



This is a problem as the endpoint has no authentication at all and anyone who can reach port 9334 gets a free browser. For this reason it's best to run this server behind a firewall or on a private network.

Finally, what about setting up the server from the command line? The CLI has a `server` command so we tried giving it a port:

shell

```shell
python -m camoufox server --port 8888
```



Which just prints:

text

```text
No such option '--port'.
```



Turns out the `server` command doesn't take any flags, so the port, host and every other launch option have to be set through `launch_server()` in Python like we did above.

### What Belongs in a Camoufox Docker Image?

What about running Camoufox in Docker? Let's take a look at the `Dockerfile` we used for a small scraper:

dockerfile

```dockerfile
FROM python:3.12-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
        xvfb libgtk-3-0 libx11-xcb1 libasound2 libdbus-glib-1-2 \
    && rm -rf /var/lib/apt/lists/*

ENV PIP_DEFAULT_TIMEOUT=300
RUN pip install --no-cache-dir "camoufox[geoip]==0.5.8"

RUN python -m camoufox fetch

WORKDIR /app
COPY scraper.py .

CMD ["python", "scraper.py"]
```



Note that the slim image has no Xvfb, so the first `RUN` adds it for `headless="virtual"` along with a few libraries for Firefox. As for `PIP_DEFAULT_TIMEOUT`, we only added it after a pip `ReadTimeoutError` killed one of our early builds!

Our `scraper.py` is a cut-down version of our first scraper that runs on a virtual display:

python

```python
from camoufox.sync_api import Camoufox

with Camoufox(headless="virtual") as browser:
    page = browser.new_page()
    page.goto("https://web-scraping.dev/products")
    print(page.locator(".product h3 a").all_inner_texts()[:2])
```



Now we can build the image and run it:

shell

```shell
docker build -t camoufox-scraper .
docker run --rm camoufox-scraper
```



Which prints our two products:

text

```text
['Box of Chocolate Candy', 'Dark Red Energy Potion']
```



The image came out at 5.07 GB which surprised us, so let's take a look at `docker history camoufox-scraper` to see where all of it went:

text

```text
0B  CMD ["python" "scraper.py"]
4.1kB   COPY scraper.py . # buildkit
0B  WORKDIR /app
2.67GB  RUN /bin/sh -c python -m camoufox fetch # bu…
298MB   RUN /bin/sh -c pip install --no-cache-dir "c…
0B  ENV PIP_DEFAULT_TIMEOUT=300
316MB   RUN /bin/sh -c apt-get update && apt-get ins…
0B  CMD ["python3"]
```



Turns out the `fetch` layer is 2.67 GB on its own since it holds the browser, the GeoIP database and the uBlock Origin addon Camoufox downloads with it.

So what if we change the scraper? Here's the next `docker build` after we edited `scraper.py`:

text

```text
#7 [3/6] RUN pip install --no-cache-dir "camoufox[geoip]==0.5.8"
#7 CACHED
#8 [4/6] RUN python -m camoufox fetch
#8 CACHED
#9 [5/6] WORKDIR /app
#9 CACHED
#10 [6/6] COPY scraper.py .
#10 DONE 0.2s
```



Just like that, the rebuild took 2.19 seconds as only the `COPY` step ran. If `fetch` came after our code, every scraper edit would download the browser again!

Interestingly, our second build wasn't this quick. The `python:3.12-slim` tag had moved to a new image between our first two builds so Docker threw away every layer. It downloaded the 1.3 GB browser again and took 4 minutes 15 seconds. For this reason it's best to pin the base image by digest as well, like `python:3.12-slim@sha256:<digest>`.

What about running `launch_server()` in the container? That works too, but the clients need the same major and minor Playwright version as the server, just like with `connect()` earlier. So we checked ours with `pip show playwright` inside the image and it reported `Version: 1.62.0`, the same as our client.



## Where Does Camoufox Still Get Detected or Break?

Camoufox plugs most of the well known automation leaks and `navigator.webdriver` was false in every one of our runs. That being said, it can still be identified and it can still break, so let's take a look at the issues we ran into.

Most widely known example of this is the browser engine itself. Camoufox can pretend to be Windows or macOS but underneath it's always Firefox, and javascript behaves slightly differently in every engine. To see this, let's run the same small script in Camoufox and in Playwright's Chromium:

shell

```shell
playwright install chromium
```



python

```python
from camoufox.sync_api import Camoufox
from playwright.sync_api import sync_playwright

probe = """
function probe() { return new Error("x").stack.split("\\n")[0]; }
document.body.dataset.stack = probe();
document.body.dataset.moz = "mozInnerScreenX" in window;
"""


def check(browser, name):
    page = browser.new_page()
    page.goto("https://web-scraping.dev/products")
    page.add_script_tag(content=probe)
    print(name, page.evaluate("JSON.stringify(document.body.dataset)"))


with Camoufox(headless=True, os="windows") as browser:
    check(browser, "camoufox:")

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    check(browser, "chromium:")
    browser.close()
```



Which prints:

text

```text
camoufox: {"stack":"probe@https://web-scraping.dev/products line 7 > injectedScript:2:27","moz":"true"}
chromium: {"stack":"Error: x","moz":"false"}
```



Notice the first line of the error stack: Firefox's SpiderMonkey engine prints `probe@` followed by the script URL while Chrome's V8 prints `Error: x`. Not only that, but Firefox has its own `mozInnerScreenX` variable which Chrome doesn't have at all!

Unfortunately, no fingerprint value can patch this. As the README puts it: "Some WAFs test SpiderMonkey engine behaviour, which no amount of spoofing turns into Chromium."

To add, the fingerprint values themselves can disagree with each other. The README warns that "Thousands of values must agree with each other, and anti-bot vendors look for the one that does not." The 2026 status note mentions newly discovered inconsistencies too and the maintainers say Camoufox may not suit stable production use.

Camoufox doesn't come with a [CAPTCHA solver](https://scrapfly.io/blog/posts/how-to-bypass-captcha-web-scraping) either. Its `disable_coop=True` option lets Playwright's clicks reach cross-origin frames like the Turnstile checkbox, but the solving part is up to us - see our [Cloudflare Turnstile guide](https://scrapfly.io/blog/posts/how-to-bypass-cloudflare-turnstile).

Finally, there's memory. To see how much Camoufox takes, we read the proportional memory (PSS) of every browser process our script started from Linux's `/proc`, skipping the Playwright driver:

python

```python
import os
import sys
from pathlib import Path

from camoufox import DefaultAddons
from camoufox.sync_api import Camoufox


def children():
    tree = {}
    for stat in Path("/proc").glob("[0-9]*/stat"):
        try:
            pid, rest = stat.read_text().split(" (", 1)
            name, fields = rest.rsplit(") ", 1)
            tree.setdefault(int(fields.split()[1]), []).append((int(pid), name))
        except (OSError, ValueError):
            continue
    found, todo = [], [os.getpid()]
    while todo:
        for pid, name in tree.get(todo.pop(), []):
            found.append((pid, name))
            todo.append(pid)
    return found


def pss_mb(pid):
    for line in Path(f"/proc/{pid}/smaps_rollup").read_text().splitlines():
        if line.startswith("Pss:"):
            return int(line.split()[1]) / 1024
    return 0


kwargs = {"exclude_addons": [DefaultAddons.UBO]} if "noubo" in sys.argv else {}
with Camoufox(headless=True, **kwargs) as browser:
    page = browser.new_page()
    page.goto("https://web-scraping.dev/products")
    page.wait_for_timeout(3000)
    procs = [(pid, name, pss_mb(pid)) for pid, name in children() if name not in ("node", "MainThread")]
    for pid, name, mb in procs:
        print(f"  {name:<20} {mb:7.1f} MB")
    print(f"total PSS: {sum(mb for *_, mb in procs):.0f} MB across {len(procs)} processes")
```



The script gives the page 3 seconds to settle, then prints the PSS of each child process. On build 156.0.1-beta.36 that came to 942 MB:

text

```text
  camoufox-bin           413.3 MB
  forkserver               8.5 MB
  Socket Process           6.9 MB
  WebExtensions          434.0 MB
  RDD Process             17.6 MB
  Web Content             55.7 MB
  Utility Process          6.1 MB
total PSS: 942 MB across 7 processes
```



Interestingly, `camoufox-bin` is not the biggest process here - `WebExtensions` is, and it's running the [uBlock Origin addon](https://scrapfly.io/blog/posts/how-to-use-browser-extensions-with-playwright-puppeteer-and-selenium) Camoufox bundles by default. With our `noubo` flag, which passes `exclude_addons=[DefaultAddons.UBO]`, `WebExtensions` drops to 66.7 MB and the total to 532 MB!

That was our worst run though. Across five runs with uBlock Origin the total was anywhere between 710 and 942 MB, as `WebExtensions` alone swung from 242 to 434 MB. Without it, the total stayed at 532 to 559 MB.

A later rerun gave us 672 to 857 MB with it and 530 to 535 MB without it. That being said, uBlock Origin also blocks ads and trackers which saves proxy bandwidth, so it's not always worth dropping.

Camoufox also warns us when we use options that are known to leak. Let's take a look at what happens when we turn on two of them:

python

```python
from camoufox.sync_api import Camoufox

with Camoufox(headless=True, block_images=True, block_webgl=True) as browser:
    page = browser.new_page()
    page.goto("https://web-scraping.dev/products")
    print(page.locator(".product h3 a").all_inner_texts()[:2])
```



The scraper still got our two products, but first we got this:

text

```text
warnings_check.py:3: LeakWarning: Blocking image requests has been reported to cause detection issues on major WAFs.
If this is intentional, pass `i_know_what_im_doing=True`.
  with Camoufox(headless=True, block_images=True, block_webgl=True) as browser:
warnings_check.py:3: LeakWarning: Disabling WebGL is not recommended. Many WAFs will check if WebGL is enabled.
If this is intentional, pass `i_know_what_im_doing=True`.
  with Camoufox(headless=True, block_images=True, block_webgl=True) as browser:
['Box of Chocolate Candy', 'Dark Red Energy Potion']
```



These are `LeakWarning`s, and all but the proxy one can be silenced with `i_know_what_im_doing=True`. It's better to listen to them though, so here are four of these warnings and what to do about them:

| Option | Warning Camoufox prints | What to do |
|---|---|---|
| `proxy` without `geoip` | "When using a proxy, it is heavily recommended that you pass `geoip=True`." | Add `geoip=True` |
| `block_images=True` | "Blocking image requests has been reported to cause detection issues on major WAFs." | Keep images on |
| `block_webgl=True` | "Disabling WebGL is not recommended. Many WAFs will check if WebGL is enabled." | Keep WebGL on |
| `disable_coop=True` | "Disabling Cross-Origin-Opener-Policy (COOP) handling can potentially be detected by sophisticated WAFs." | Use it only for challenge frames |

Each new browser build fixes some leaks and can introduce new ones, so it's a good idea to pin the build and run a quick canary check on every upgrade. Our user agent, language and timezone checks from earlier take only a few seconds, plus one real target we're allowed to test.



## When Should You Move From Self-Hosted Camoufox to a Remote Browser?

Running Camoufox ourselves turned into a job of its own. Each instance took up to 942 MB in our runs, the builds have to be pinned and our own server had no authentication at all!

With our `launch_server()` setup the Firefox identity stays on our hardware and proxies. Unfortunately, so do the servers, the upgrades and the endpoint security! Here are the three remote routes we tried and what each of them takes off our hands:

| Route | Engine | You run | Session model | Fits when |
|---|---|---|---|---|
| Camoufox `launch_server()` | Firefox (Camoufox) | Servers, proxies, upgrades | One fingerprint per server | Targets accept Firefox, modest volume |
| Scrapfly Cloud Browser | Chromium (Scrapium) | Your Playwright code | Per session, resumable | Clicks, forms and logins at scale |
| Scrapfly Web Scraping API | Managed rendering | One HTTP call per page | Per request | Only the rendered HTML |

Interestingly, Cloud Browser runs as Chromium so none of the Firefox-only Camoufox options carry over, while the Web Scraping API has no browser in our code at all. For the bigger picture, see [web scraping with cloud browsers](https://scrapfly.io/blog/posts/web-scraping-with-cloud-browsers).

### Keep Your Playwright Code on Scrapfly Cloud Browser Over Chrome CDP

[Scrapfly Cloud Browser](https://scrapfly.io/products/cloud-browser-api) runs Scrapium, Scrapfly's Chromium build with "550+ patched Chromium source files", and Playwright connects to it over CDP. Our page code stays the same, only the connection changes:

python

```python
import os
import time

from playwright.sync_api import sync_playwright

ws = (
    "wss://browser.scrapfly.io"
    f"?api_key={os.environ['SCRAPFLY_API_KEY']}"
    "&proxy_pool=public_datacenter_pool&os=windows&country=us"
)

with sync_playwright() as p:
    start = time.perf_counter()
    browser = p.chromium.connect_over_cdp(ws)
    print(f"connected in {time.perf_counter() - start:.2f}s")
    context = browser.contexts[0]
    page = context.pages[0] if context.pages else context.new_page()
    page.goto("https://web-scraping.dev/products")
    print(page.locator(".product h3 a").all_inner_texts())
    print(page.evaluate("navigator.userAgent"))
    print("webdriver:", page.evaluate("navigator.webdriver"))
    print("timezone:", page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone"))
    browser.close()
```



The only real change is `p.chromium.connect_over_cdp()` in place of `Camoufox()`, and since the remote browser already comes with a context we just reuse it. Proxy pool, OS and country go into the URL as query parameters and the API key comes from the environment:

text

```text
connected in 1.89s
['Box of Chocolate Candy', 'Dark Red Energy Potion', 'Teal Energy Potion', 'Red Energy Potion', 'Blue Energy Potion']
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36
webdriver: False
timezone: America/New_York
```



It connected in 1.89 seconds and found the same five products, this time from a Windows Chrome 155 browser. Notice the `America/New_York` timezone too - we didn't set up any GeoIP as `country` sets the timezone, locale and IP location together. The whole run took 6.6 seconds.

The [Cloud Browser Playwright docs](https://scrapfly.io/docs/cloud-browser-api/playwright) list the other parameters, such as `proxy_pool` for residential IPs. Note that a session runs for 15 minutes by default and 30 at most.

For longer jobs, Cloud Browser has [session resume](https://scrapfly.io/docs/cloud-browser-api/session-resume). Let's give it a try by saving a value in one connection and reading it back from a second one:

python

```python
import os
import time
import urllib.request

from playwright.sync_api import sync_playwright

key = os.environ["SCRAPFLY_API_KEY"]
ws = (
    "wss://browser.scrapfly.io"
    f"?api_key={key}&session=camoufox-demo&auto_close=false"
    "&proxy_pool=public_datacenter_pool&os=windows&country=us"
)


def open_page(browser):
    context = browser.contexts[0]
    return context.pages[0] if context.pages else context.new_page()


try:
    with sync_playwright() as p:
        page = open_page(p.chromium.connect_over_cdp(ws))
        page.goto("https://web-scraping.dev/products?page=2")
        page.evaluate("localStorage.setItem('last_page', '2')")
        print("first run saved page:", page.evaluate("localStorage.getItem('last_page')"))
        print("ip:", page.evaluate("fetch('https://api.ipify.org').then(r => r.text())"))

    time.sleep(10)

    with sync_playwright() as p:
        start = time.perf_counter()
        page = open_page(p.chromium.connect_over_cdp(ws))
        print(f"reconnected in {time.perf_counter() - start:.2f}s")
        page.goto("https://web-scraping.dev/products")
        print("second run found page:", page.evaluate("localStorage.getItem('last_page')"))
        print("ip:", page.evaluate("fetch('https://api.ipify.org').then(r => r.text())"))
finally:
    stop = f"https://browser.scrapfly.io/session/camoufox-demo/stop?api_key={key}"
    print("stop:", urllib.request.urlopen(urllib.request.Request(stop, method="POST")).status)
```



Here, `session=camoufox-demo` names the session and `auto_close=false` keeps the browser running after we disconnect. The first connection saves `last_page` in `localStorage` and prints its exit IP, then we wait 10 seconds and connect again with the same URL:

text

```text
first run saved page: 2
ip: 180.149.13.41
reconnected in 2.78s
second run found page: 2
ip: 180.149.13.41
stop: 200
```



As we can see, the second connection found `last_page` and used the same `180.149.13.41` exit IP, so a scraper that disconnects can continue where it stopped. Note that with `auto_close=false` the session runs until it times out. So stopping it is up to us, which our `finally` block does with a `POST` to `/session/camoufox-demo/stop`.

### Use the Web Scraping API When You Only Need Rendered HTML

When we don't need clicks or forms, the [Web Scraping API](https://scrapfly.io/products/web-scraping-api) returns the rendered page in one call. It needs `pip install scrapfly-sdk parsel`, as the SDK's `selector` property uses parsel:

python

```python
import os

from scrapfly import ScrapeConfig, ScrapflyClient

client = ScrapflyClient(key=os.environ["SCRAPFLY_API_KEY"])
result = client.scrape(ScrapeConfig(
    url="https://web-scraping.dev/products",
    render_js=True,
    unblocker=True,
    country="us",
))
print(result.selector.css(".product h3 a::text").getall())
```



Here `render_js` renders the page in a browser on Scrapfly's side and `unblocker` turns on the [Unblocker](https://scrapfly.io/docs/scrape-api/unblocker). Running it prints:

text

```text
['Box of Chocolate Candy', 'Dark Red Energy Potion', 'Teal Energy Potion', 'Red Energy Potion', 'Blue Energy Potion']
```



Not only that, but there's no browser, proxy or GeoIP to manage on our side either - just the HTML and a parser.



## FAQ

Is Camoufox still maintained in 2026?Yes. To check, we can use PyPI's JSON API, which lists the upload date of every release:

python

```python
import requests

releases = requests.get("https://pypi.org/pypi/camoufox/json").json()["releases"]
for version in ["0.4.11", "0.5.3", "0.5.6", "0.5.7", "0.5.8"]:
    print(version, releases[version][0]["upload_time"][:10])
```



text

```text
0.4.11 2025-01-29
0.5.3 2026-07-15
0.5.6 2026-09-06
0.5.7 2026-10-04
0.5.8 2026-10-06
```



As we can see, there's a gap of almost a year and a half between 0.4.11 and 0.5.3, which the project's status page acknowledges. 0.5.9 followed on November 1, 2026, but the README still calls Camoufox under development, and everything in this article was tested on 0.5.8.







Can you use Camoufox from JavaScript or Node.js?Yes. The project publishes a `camoufox` package on npm, versioned in step with the Python package. Alternatively, the `launch_server()` setup from the remote server section works from any Playwright client through `firefox.connect()`, [Node.js](https://scrapfly.io/blog/posts/web-scraping-with-nodejs) included. Note that the client needs the same major and minor Playwright version as the server.







Does Camoufox include a CAPTCHA solver?No, Camoufox doesn't solve CAPTCHAs, but with `disable_coop=True` clicks can reach cross-origin frames like the Turnstile checkbox. Note that Camoufox warns about this option at launch, as it "can potentially be detected by sophisticated WAFs". For more on getting past these challenges, see the Turnstile guide in the limits section.







What are the best alternatives to Camoufox?It depends on the engine we need. For a Chromium identity, Nodriver, Patchright and SeleniumBase UC Mode are the popular open-source options, and our stealth browsers article from the first section compares them with Camoufox by speed and maintenance.







Is it legal to scrape with an anti-detect browser like Camoufox?It depends on what we scrape and how, not on the browser. Camoufox is a browser like any other, so the usual web scraping rules apply. That being said, attention should be paid to public versus logged-in pages, terms we agreed to and personal data laws like GDPR.









## Summary

Camoufox is an open-source Firefox build that patches its fingerprint in C++ rather than in page JavaScript. We've taken a look at how to install it, scrape pages with its sync and async APIs, set up proxies with GeoIP and run it [headless](https://scrapfly.io/blog/posts/how-to-scrape-with-headless-firefox), in Docker and as a remote Playwright server.

We've also covered a few gotchas. Camoufox looks the proxy's exit IP up once at launch, so it's one sticky proxy session per browser, and authenticated proxies only work over HTTP as SOCKS5 authentication fails at launch. Note that `launch_server()` has no authentication at all, so we keep it behind a firewall or on a private network. Memory was another surprise, as one browser took anywhere from 672 to 942 MB and most of that swing came from the bundled uBlock Origin addon. Also, 0.5.7 swapped BrowserForge for fpgen and 0.5.8 followed two days later, so we pin the build.

For targets where Firefox gets blocked or that need clicks, forms and logins at scale, we moved the same Playwright code to Scrapfly Cloud Browser over CDP. Or, we used the Web Scraping API when only the HTML is needed. That being said, where Firefox gets through, self-hosted Camoufox is a great choice!



Legal Disclaimer and PrecautionsThis tutorial covers popular web scraping techniques for education. Interacting with public servers requires diligence and respect:

- Do not scrape at rates that could damage the website.
- Do not scrape data that's not available publicly.
- Do not store PII of EU citizens protected by GDPR.
- Do not repurpose *entire* public datasets which can be illegal in some countries.

Scrapfly does not offer legal advice but these are good general rules to follow. For more you should consult a lawyer.

 

   [  Add as a preferred source ](https://google.com/preferences/source?q=scrapfly.io) Table of Contents















 

  Table of Contents

- [Key Takeaways](#key-takeaways)
- [What Is Camoufox and How Does It Hide Playwright Automation?](#what-is-camoufox-and-how-does-it-hide-playwright-automation)
- [How Do You Install Camoufox in Python?](#how-do-you-install-camoufox-in-python)
- [How Do You Scrape a Page With Camoufox?](#how-do-you-scrape-a-page-with-camoufox)
- [Run a First Scraper With the Camoufox Sync API](#run-a-first-scraper-with-the-camoufox-sync-api)
- [Scale Camoufox With the Async API and One Browser per Worker](#scale-camoufox-with-the-async-api-and-one-browser-per-worker)
- [How Do You Use Proxies and GeoIP With Camoufox?](#how-do-you-use-proxies-and-geoip-with-camoufox)
- [Pass a Playwright Proxy Dictionary With geoip=True](#pass-a-playwright-proxy-dictionary-with-geoip-true)
- [Does GeoIP Set the Page Timezone Too?](#does-geoip-set-the-page-timezone-too)
- [Should Camoufox Use Sticky or Rotating Proxy Sessions?](#should-camoufox-use-sticky-or-rotating-proxy-sessions)
- [Give Each Browser Context Its Own Proxy With NewContext](#give-each-browser-context-its-own-proxy-with-newcontext)
- [How Do You Run Camoufox Headless on a Server?](#how-do-you-run-camoufox-headless-on-a-server)
- [headless=True vs headless="virtual" (Xvfb) on Linux](#headless-true-vs-headless-virtual-xvfb-on-linux)
- [Run Camoufox as a Remote Playwright Server](#run-camoufox-as-a-remote-playwright-server)
- [What Belongs in a Camoufox Docker Image?](#what-belongs-in-a-camoufox-docker-image)
- [Where Does Camoufox Still Get Detected or Break?](#where-does-camoufox-still-get-detected-or-break)
- [When Should You Move From Self-Hosted Camoufox to a Remote Browser?](#when-should-you-move-from-self-hosted-camoufox-to-a-remote-browser)
- [Keep Your Playwright Code on Scrapfly Cloud Browser Over Chrome CDP](#keep-your-playwright-code-on-scrapfly-cloud-browser-over-chrome-cdp)
- [Use the Web Scraping API When You Only Need Rendered HTML](#use-the-web-scraping-api-when-you-only-need-rendered-html)
- [FAQ](#faq)
- [Summary](#summary)
 
    Join the Newsletter  Get monthly web scraping insights 

 

  



Scale Your Web Scraping

Anti-bot bypass, browser rendering, and rotating proxies, all in one API. Start with 1,000 free credits.

  No credit card required  1,000 free API credits  Anti-bot bypass included 

 [Start Free](https://scrapfly.io/register) [View Docs](https://scrapfly.io/docs/onboarding) 

 Not ready? Get our newsletter instead. 

 

 ## Related Articles

 [     

 python selenium 

### Intro to Web Scraping Using Selenium Grid

In this guide, you will learn about installing and configuring Selenium Grid with Docker and how to use it for web scrap...

 

 ](https://scrapfly.io/blog/posts/intro-to-web-scraping-using-selenium-grid) [     

 python headless-browser 

### How to Scrape Dynamic Websites Using Headless Web Browsers

Introduction to using web automation tools such as Puppeteer, Playwright, Selenium and ScrapFly to render dynamic websit...

 

 ](https://scrapfly.io/blog/posts/scraping-using-browsers) [     

 ai tools 

### 5 MCP Servers for Web Scraping and Browser Automation in 2026

Compare five MCP servers by the work they can actually do: protected-site scraping, browser control, debugging, static f...

 

 ](https://scrapfly.io/blog/posts/best-mcp-servers-for-web-scraping) 

  ## Related Questions

- [ Q How to Rotate Proxies in Scrapy ](https://scrapfly.io/blog/answers/scrapy-spiders-proxy-rotation)
- [ Q How to use headless browsers with scrapy? ](https://scrapfly.io/blog/answers/how-to-use-headless-browsers-with-scrapy)
 
  



   



 Run headless browsers at scale, **1,000 free credits** [Start Free](https://scrapfly.io/register)