Camoufox Tutorial: Stealth Firefox Automation with Proxies and GeoIP Matching (2026)

Developer guide · Last reviewed 20 October 2026 · 16 min read

Camoufox is an open-source Firefox build for web scraping and AI agents that reports a different, internally consistent device from inside the browser’s C++ code, and that you drive with ordinary Playwright calls. This Camoufox tutorial covers what most guides skip: how geoip derives timezone, locale and WebRTC IP from your proxy, why that needs a sticky session, how to keep one identity per profile, how to run browsers concurrently from Python, and how to check the result before you trust it.

The short version

Install with pip install -U "camoufox[geoip]", download the browser with camoufox fetch, and replace your Playwright launch with Camoufox(...) or AsyncCamoufox(...). Pass the proxy as a Playwright proxy dict with an http:// server and set geoip=True so timezone, locale, geolocation and the WebRTC IP follow the exit IP. Use a sticky session for every geo-matched browser, because the lookup and the page must see the same IP. Give each profile its own session, and verify what pages actually see before you scale up.

Versions in this guide. At the time of writing the latest stable camoufox release on PyPI is 0.5.8, with 0.5.9 pre-releases out, and the README still warns that the project is under development and may not suit stable production use. A few options documented on the main branch, such as canvas_noise and headless="virtual-gpu", are in the 0.5.9 pre-releases but not 0.5.8. Check camoufox version against the README.

Key takeaways

  • Camoufox is a patched Firefox, not a plugin: values are answered in C++ and Playwright runs in an isolated world the page cannot see.
  • Existing Playwright code works once the launch becomes Camoufox() or AsyncCamoufox().
  • geoip=True looks up the exit IP through your proxy once, at launch, and sets timezone, locale, geolocation and WebRTC IP from it.
  • Because that lookup is a snapshot, geo-matched browsers need sticky sessions.
  • A fresh identity is drawn on every launch. A persistent profile keeps cookies, not the device, unless you pin a preset.
  • Each browser is a full Firefox process: bound concurrency, reuse browsers and recycle them.
  • Camoufox can still be detected and only presents Firefox. Verify on test pages and stay within authorized, public-data use.

What Camoufox Is

A Firefox build, a Python launcher and a TypeScript launcher

Camoufox describes itself as an open-source anti-detect browser built for web scraping and AI agents. In practice it is three things that ship together.

A patched FirefoxPatches answer navigator, screen, WebGL, fonts, audio, voices, timezone, locale and WebRTC values from C++, so a page reads native values rather than JavaScript overrides.
A Python launcherThe camoufox package draws a device identity, checks that its parts agree, passes it to the browser and returns normal Playwright objects.
A TypeScript launcherA port of the Python package for Node.js that uses playwright-core, with the same options and the same identity model.

The identity is drawn from fpgen, a fingerprint model of real-world traffic (older docs and tutorials still mention BrowserForge, which the current package replaced), and a coherence step rejects combinations no real machine produces, such as a Windows user agent with an Apple GPU. The build also strips telemetry and Mozilla background services and installs uBlock Origin by default.

On maintenance: the project’s stealth page says there was roughly a year-long gap in maintenance and that development is active again. The full browser source has been public since early 2026; the browser is MPL-2.0 and the launchers are MIT. Pin versions and test before upgrading.

How It Differs from Chromium Stealth Plugins

Where the spoofing happens matters more than what is spoofed

Most Chromium stealth setups, such as Playwright or Puppeteer with a stealth plugin, run JavaScript in the page before the site’s own scripts to redefine navigator.webdriver, plugins, WebGL getters and so on. Detection scripts know the weaknesses:

  • Overrides are observable. A patched function’s toString() or property descriptor can reveal it is not native.
  • Coverage is uneven. Workers and iframes are often missed, so the page can compare contexts.
  • Headers drift from script. User-Agent and Accept-Language headers must be set separately from navigator.
  • The protocol leaves traces. CDP-driven automation exposes bindings and flags pages can probe; see our WebDriver BiDi vs CDP comparison.

Camoufox moves that work into the engine. Playwright drives Firefox through Juggler, a separate module inside Firefox, not CDP. Camoufox patches Juggler so Playwright works on its own isolated copy of the page: selectors, evaluate calls and init scripts run there, and the real page never sees getters fire or listeners appear. Input goes through Firefox’s normal input handlers, and headers are generated from the same values as navigator.

JavaScript stealth patches the answers a page gets. Camoufox changes the browser that gives them.

The trade-off: Camoufox only presents Firefox, and its README notes that some WAFs test SpiderMonkey engine behavior that no spoofing turns into Chromium. If you need Chrome specifically, it is the wrong tool. For background on headless engines, see Headless Browsers 101.

Install Camoufox and Fetch the Browser

Two commands, plus one requirement people miss

The Python package needs Python 3.10 or newer, and it installs Playwright below 1.63 as a dependency. The geoip extra adds the GeoIP database that geoip= needs. The database is downloaded and refreshed once it is a week old.

Example 1 — install the package and the browser (shell)
python3 -m venv .venv && source .venv/bin/activate
pip install -U "camoufox[geoip]"

# Download the browser build paired with this package release (plus fpgen's model)
camoufox fetch            # or: python -m camoufox fetch

# Print package, Playwright and browser versions, GeoIP database and paths
camoufox version

camoufox fetch installs the browser build this package release was tested with, plus fpgen’s model, both pinned by checksum. Skip it and the first launch downloads them, which is slow inside a container, so in Docker run fetch at build time as the user that owns the Python environment. camoufox test opens a headed browser with the Playwright inspector; camoufox remove deletes everything downloaded.

For Node.js the equivalent is npm install camoufox playwright-core followed by npx camoufox fetch. The TypeScript package requires Node.js 22.15 or newer.

Your First Camoufox Script

Change the launch, keep your Playwright code

The sync and async entry points are context managers. Camoufox lives in camoufox.sync_api and AsyncCamoufox in camoufox.async_api. Each yields a normal Playwright Browser and closes it on exit.

Example 2 — a first sync script (Python)
from camoufox.sync_api import Camoufox

with Camoufox(headless=True) as browser:
    page = browser.new_page()
    page.goto("https://example.com", wait_until="domcontentloaded")

    print(page.title())
    print(page.evaluate("navigator.userAgent"))
    print(page.evaluate("navigator.webdriver"))

    page.screenshot(path="example.png", full_page=True)

Run it twice and print the user agent each time. You will usually see different devices, because Camoufox draws a fresh identity on every launch unless you pin one. That is useful for one-off collection and wrong for anything that logs in or keeps cookies, which we return to in the profiles section.

The headless option controls how the window is rendered:

headless=What runsNotes
False (default)A normal windowNeeds a display
TrueFirefox headlessCamoufox patches the headless tells it knows about
“virtual”Headed browser on a private Xvfb displayLinux only; needs Xvfb; software rendering
“virtual-gpu”Headed browser on a headless Xorg bound to one GPULinux only; newer releases; needs one-time setup

The docs recommend the virtual display as a fallback in case headless mode becomes detectable. The Xvfb display is 1×1 by default; set CAMOUFOX_VIRTUAL_DISPLAY_SIZE (for example 1920x1080x24) if you need a real size.

Camoufox Proxy Setup and GeoIP Matching

The proxy changes the IP; geoip makes the browser agree with it

A proxy changes where traffic exits, not what the browser says about itself. A browser on a German residential IP that reports America/Los_Angeles, en-US and a WebRTC candidate with your server’s real address is easy to flag. Camoufox’s geoip option closes that gap.

With geoip=True and a proxy, the launcher asks a public “what is my IP” service through the proxy before the browser starts, resolves that IP in the local GeoIP database, and sets timezone, locale, geolocation and WebRTC IP from it. Without an explicit locale, it picks one from the languages spoken in that region. A proxy without geoip triggers a warning recommending it.

Figure 1 — what happens at launch with proxy and geoip=True
Your Playwright code
 |
 v
camoufox launcher
 |-- 1. draw device identity (fpgen) + coherence check
 |-- 2. GET public-IP service  --via proxy-->  exit IP 203.0.113.7
 |-- 3. GeoIP DB: 203.0.113.7 -> timezone, locale, lat/long
 |-- 4. pass identity + geo to the browser
 v
Patched Firefox (values answered in C++)
 |
 v
ProxyEmpire gateway  v2.proxyempire.io:5000  (sticky session)
 |
 v
Target site sees: exit IP 203.0.113.7, matching timezone/locale/WebRTC

With ProxyEmpire, the gateway is v2.proxyempire.io on port 5000, authenticated by username and password. Targeting and session settings are encoded in the username, so do not build it by hand: open Proxy Manager → Connection Builder, choose location and rotation, and copy the generated credentials. Test them there or with curl -x http://USERNAME:[email protected]:5000 https://api.ipify.org.

Example 3 — Camoufox with a ProxyEmpire proxy and geoip=True (Python)
import os

from camoufox.sync_api import Camoufox

# Copy the username (it encodes country, rotation and session) from
# Proxy Manager -> Connection Builder. Keep credentials out of the code.
PROXY = {
    "server": "http://v2.proxyempire.io:5000",
    "username": os.environ["PE_USERNAME"],
    "password": os.environ["PE_PASSWORD"],
}

with Camoufox(proxy=PROXY, geoip=True, headless=True) as browser:
    page = browser.new_page()

    page.goto("https://api.ipify.org", wait_until="domcontentloaded")
    print("exit IP:  ", page.inner_text("body").strip())

    print("timezone: ", page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone"))
    print("languages:", page.evaluate("navigator.languages"))

Use the http:// form of the gateway. It is what Camoufox documents, Playwright’s Chromium rejects authenticated SOCKS5 outright (so the same config stays portable), and an HTTP proxy tunnels HTTPS fine. If the launch fails with a proxy error, see our proxy error codes guide; a 407 means the proxy credentials, not the target.

For most scraping and agent work, rotating residential proxies with country or city targeting are the natural pairing, and Camoufox’s docs also recommend residential IPs. For one IP over days, static residential (ISP) proxies remove rotation entirely. Camoufox only draws desktop identities, worth remembering with mobile proxies.

Sticky Sessions, One per Profile

The lookup is a snapshot, so the IP has to stay put

This is the part most tutorials miss. geoip=True runs once, at launch; after that the browser’s timezone, locale and WebRTC IP are fixed. Whether they stay correct depends on rotation mode:

Rotation modeWhat geoip sees vs the pageUse with Camoufox?
New IP every requestThe lookup gets one IP, page requests get othersNot with geoip; WebRTC IP and possibly timezone won’t match
Same IP (sticky)Same IP for the lookup and the pageYes, the default choice
Custom rotation (up to 2 h)Matches until the session rotatesYes, if you relaunch the browser before it rotates

Two details make this sharper. Even with country targeting, a new IP can be in another timezone. And in the versions we reviewed, the launcher caches the public-IP lookup per proxy string within a Python process, so relaunching with an identical rotating proxy string can reuse a stale IP. Distinct sticky usernames per profile avoid both. In ProxyEmpire, Same IP mode puts a session ID (sid) in the username and you can create your own 8-digit IDs; see the sticky sessions docs and our sticky vs rotating proxies guide.

For certainty, do the lookup yourself and pass the IP as a string. Camoufox skips its own lookup, and you can confirm the page sees the same address:

Example 4 — pin geoip to the session’s exit IP and verify it (Python)
import os
from urllib.parse import quote

import requests
from camoufox.sync_api import Camoufox

GATEWAY = "v2.proxyempire.io:5000"


def lookup_exit_ip(username, password):
    """Ask for the exit IP through the same sticky session the browser will use."""
    url = f"http://{quote(username, safe='')}:{quote(password, safe='')}@{GATEWAY}"
    resp = requests.get(
        "https://api.ipify.org",
        proxies={"http": url, "https": url},
        timeout=15,
    )
    resp.raise_for_status()
    return resp.text.strip()


username = os.environ["PE_USERNAME"]   # a sticky ("Same IP") username
password = os.environ["PE_PASSWORD"]
proxy = {"server": f"http://{GATEWAY}", "username": username, "password": password}

expected_ip = lookup_exit_ip(username, password)

# A string skips Camoufox's own lookup and derives the geo settings from this IP.
with Camoufox(proxy=proxy, geoip=expected_ip, headless=True) as browser:
    page = browser.new_page()
    page.goto("https://api.ipify.org", wait_until="domcontentloaded")
    seen_ip = page.inner_text("body").strip()

    if seen_ip != expected_ip:
        raise RuntimeError(f"session moved: launched for {expected_ip}, page sees {seen_ip}")
    print("identity and exit IP agree:", seen_ip)

One profile, one identity, one session

A profile for repeat visits, such as a monitor that keeps a store’s region and consent cookies, needs three things to stay stable together: exit IP, device identity and storage. persistent_context=True with a user_data_dir keeps storage (and yields a BrowserContext, not a Browser), but the device is still drawn fresh each launch. The same cookies from a different “machine” every morning is its own inconsistency.

So pass the same preset with fingerprint_preset= every launch, and give each profile one sticky session for good: export a batch from the dashboard as a host:port:user:pass proxy list and assign one line per profile.

Example 5 — persistent profiles with a pinned preset and a dedicated sticky session (Python)
import json
from pathlib import Path

from camoufox.sync_api import Camoufox
from camoufox.fingerprints import get_random_preset  # internal helper, see note

PROFILES_DIR = Path("profiles")


def load_proxy_list(path):
    """Parse a host:port:user:pass export from the ProxyEmpire dashboard."""
    proxies = []
    for line in Path(path).read_text().splitlines():
        line = line.strip()
        if not line:
            continue
        host, port, user, password = line.split(":", 3)
        proxies.append({"server": f"http://{host}:{port}", "username": user, "password": password})
    return proxies


def load_or_create_profile(name, proxy, os_name="linux"):
    """One profile = one sticky session + one pinned device preset + one Firefox profile dir."""
    meta_path = PROFILES_DIR / f"{name}.json"
    if meta_path.exists():
        return json.loads(meta_path.read_text())

    preset = get_random_preset(os=os_name)
    if preset is None:
        raise RuntimeError(f"no bundled presets for {os_name}")

    meta = {"proxy": proxy, "preset": preset}
    PROFILES_DIR.mkdir(exist_ok=True)
    meta_path.write_text(json.dumps(meta))
    return meta


def open_profile(name, meta):
    return Camoufox(
        persistent_context=True,
        user_data_dir=str(PROFILES_DIR / name),
        fingerprint_preset=meta["preset"],
        proxy=meta["proxy"],
        geoip=True,
        headless=True,
    )


if __name__ == "__main__":
    proxies = load_proxy_list("proxies.txt")
    names = ["monitor-us-01", "monitor-us-02", "monitor-us-03"]

    for name, proxy in zip(names, proxies):
        meta = load_or_create_profile(name, proxy)
        with open_profile(name, meta) as context:   # yields a BrowserContext
            page = context.new_page()
            page.goto("https://example.com", wait_until="domcontentloaded")
            print(name, page.title())

About get_random_preset. fingerprint_preset is a documented option; camoufox.fingerprints.get_random_preset is an internal helper present in 0.5.8 and the 0.5.9 pre-releases, so check it after upgrades. Use the host’s OS (newer builds turn on canvas noise for another OS, see below), and re-test profiles after browser upgrades because presets are tied to a Firefox version.

Async Concurrency with AsyncCamoufox

Workers own sessions; a queue owns the work

Every AsyncCamoufox launch starts a full Firefox process plus a geoip lookup, so launching one per URL wastes most of your time on startup. A better shape is a fixed pool: each worker owns one sticky session and one browser at a time, pulls URLs from a shared asyncio.Queue, and recycles its browser after a set number of pages. Pool size is your concurrency limit.

Example 6 — a worker pool with one sticky session per worker (Python)
import asyncio

from camoufox.async_api import AsyncCamoufox

PAGES_PER_BROWSER = 50       # recycle each browser after this many pages
NAV_TIMEOUT_MS = 45_000


async def fetch(browser, url):
    page = await browser.new_page()
    try:
        resp = await page.goto(url, wait_until="domcontentloaded", timeout=NAV_TIMEOUT_MS)
        return {"url": url, "status": resp.status if resp else None, "title": await page.title()}
    finally:
        await page.close()


async def worker(profile, proxy, queue, results):
    """One worker = one sticky session = one browser at a time."""
    while True:
        async with AsyncCamoufox(proxy=proxy, geoip=True, headless=True) as browser:
            for _ in range(PAGES_PER_BROWSER):
                url = await queue.get()
                try:
                    if url is None:          # sentinel: no more work
                        return
                    results.append({"profile": profile, **await fetch(browser, url)})
                except Exception as exc:     # log and keep the worker alive
                    results.append({"profile": profile, "url": url, "error": repr(exc)})
                finally:
                    queue.task_done()


async def main(urls, assignments):
    queue = asyncio.Queue()
    for url in urls:
        queue.put_nowait(url)
    for _ in assignments:
        queue.put_nowait(None)               # one sentinel per worker

    results = []
    tasks = [
        asyncio.create_task(worker(profile, proxy, queue, results))
        for profile, proxy in assignments.items()
    ]
    outcomes = await asyncio.gather(*tasks, return_exceptions=True)
    for profile, outcome in zip(assignments, outcomes):
        if isinstance(outcome, Exception):
            print(f"worker {profile} failed to launch: {outcome!r}")
    return results


# assignments = {"worker-01": proxy_dict_1, "worker-02": proxy_dict_2, ...}
# results = asyncio.run(main(urls, assignments))

A few notes on this pattern:

  • browser.new_page() creates a fresh context per page, so cookies do not carry between URLs. Use a persistent profile when they must.
  • Recycling on the same sticky session reuses the cached lookup, which is fine while the session holds. If sessions can expire mid-run, relaunch with the check from Example 4.
  • Job errors are recorded and the worker continues; a launch failure ends that worker and is reported after gather.

Many identities in one browser

When memory matters more than isolation, NewContext or AsyncNewContext gives each context its own identity inside one browser. With a per-context proxy, the launcher looks up that proxy’s exit IP to set the context’s timezone and WebRTC IP, and raises InvalidIP if the lookup fails.

Example 7 — one browser, several identities and sticky sessions (Python)
import asyncio

from camoufox.async_api import AsyncCamoufox, AsyncNewContext


async def main(proxies):
    async with AsyncCamoufox(headless=True) as browser:
        # Each context gets its own drawn identity and its own sticky session.
        # The launcher looks up each proxy's exit IP to set timezone and WebRTC IP.
        contexts = [await AsyncNewContext(browser, proxy=p) for p in proxies]

        pages = [await ctx.new_page() for ctx in contexts]
        await asyncio.gather(*(pg.goto("https://example.com") for pg in pages))

        for ctx in contexts:
            await ctx.close()

Locale and geolocation are separate context arguments you pass yourself. In newer builds, contexts can only claim the host’s OS unless the browser runs with canvas_noise=True. Separate browsers isolate crashes and leaks better; contexts are lighter. For fleet design, our browser cluster design for 10,000 concurrent sessions applies to Firefox fleets too.

The same launch in TypeScript

Example 8 — Camoufox with a proxy and geoip in Node.js (JavaScript)
// npm install camoufox playwright-core && npx camoufox fetch   (Node.js 22.15+)
import { Camoufox } from "camoufox";

const browser = await Camoufox({
    headless: true,
    geoip: true,
    proxy: {
        server: "http://v2.proxyempire.io:5000",
        username: process.env.PE_USERNAME,
        password: process.env.PE_PASSWORD,
    },
});

const page = await browser.newPage();
await page.goto("https://api.ipify.org");
console.log("exit IP:", (await page.innerText("body")).trim());
await browser.close();

Fingerprint Consistency: OS, Screen, Locale and Geo

Every value has to agree with every other value, and with the IP

Camoufox’s defaults are built around coherence, so override less, not more. The launcher warns when you manually set values it would otherwise generate (navigator properties, user agent, window size, geolocation); read those warnings rather than silencing them with i_know_what_im_doing=True.

OptionWhat it controlsConsistency note
os=OS of the drawn identity, or a list to pick fromAnother OS than the host affects canvas (see below)
screen=Bounds for the generated screenPrefer bounds over a fixed window=
locale=Locale(s); first is used for IntlLeave unset with geoip to match the region
geoip=Timezone, locale, geolocation, WebRTC IPNeeds a stable exit IP
humanize=Human-like cursor pathsTrue or a max seconds per move
block_images=Skips image requestsSaves bandwidth; warns it can trip some WAFs
addons=Extracted Firefox addon foldersuBlock Origin is on by default

The canvas trade-off when spoofing another OS

Canvas and WebGL pixels come from the real OS, driver and GPU, whatever the identity claims. Per the current docs, an identity on the host’s OS claims the host’s GPU, so pixels and claims agree. An identity on another OS gets random canvas readback, as in LibreWolf or Tor Browser: the real OS is hidden, but the browser reads as privacy-hardened, which is rarer than stock Firefox. On Linux servers, Linux identities are the most consistent. GPU-less servers render in software, which the docs say reads like a machine whose GPU driver failed to load.

Geo is part of the fingerprint

Match the proxy target to the identity you can produce. A US target with geoip=True gives a US timezone and English locale; forcing locale="de-DE" on top is possible but rare. A mismatch between IP country, timezone and language is one of the cheapest signals detection vendors have; our DataDome bot detection explainer shows how such signals combine with IP reputation and behavior.

Testing on Public Pages

Check what the page sees, not what you configured

Before scaling a configuration, launch it once and record what a page can observe. The script below reads the exit IP, the key navigator and Intl values, and the request headers as a server receives them.

Example 9 — record what a page sees for one configuration (Python)
import json
import os

from camoufox.sync_api import Camoufox

PROBE = """() => ({
    userAgent: navigator.userAgent,
    platform: navigator.platform,
    languages: navigator.languages,
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    utcOffsetMinutes: -new Date().getTimezoneOffset(),
    screen: [screen.width, screen.height, screen.colorDepth],
    cores: navigator.hardwareConcurrency,
    webdriver: navigator.webdriver,
})"""

proxy = {
    "server": "http://v2.proxyempire.io:5000",
    "username": os.environ["PE_USERNAME"],
    "password": os.environ["PE_PASSWORD"],
}

with Camoufox(proxy=proxy, geoip=True, headless=True) as browser:
    page = browser.new_page()
    page.goto("https://api.ipify.org", wait_until="domcontentloaded")
    report = {"exitIp": page.inner_text("body").strip(), **page.evaluate(PROBE)}

    # Request headers as the server receives them
    resp = page.goto("https://httpbin.org/headers", wait_until="domcontentloaded")
    report["headers"] = resp.json()["headers"] if resp else None

    print(json.dumps(report, indent=2))

Check that timezone and UTC offset fit the exit IP’s location, that languages agrees with Accept-Language, that the header User-Agent equals navigator.userAgent, and that webdriver is false. Then open the same configuration on public test pages such as BrowserScan (used in Camoufox’s GeoIP docs), CreepJS and the WebGL and canvas tests on camoufox.com. They show inconsistencies; they do not predict how a given site will score you.

If a site you are authorized to automate still blocks you, the README’s leak-debugging flow starts with a good question: does it block stock Firefox from the same IP? If so, the problem is the IP or your request rate, not the browser. Real detections of Camoufox can be reported through its GitHub issue template.

Performance and Resource Notes

A real browser costs real memory
  • Footprint. The project describes Camoufox as debloated, at around 200 MB per browser. Treat that as a starting estimate and measure on your own pages.
  • Startup. Each launch spawns Firefox, draws an identity and, with geoip=True, makes a lookup through the proxy. Reuse browsers and recycle on a count or timer.
  • Cache. Page and request caches are off by default to save memory; enable_cache=True restores them, and the docs note go_back()/go_forward() depend on it.
  • Bandwidth. uBlock Origin blocks ad and tracker requests by default. block_images=True saves more, but Camoufox warns it has been reported to cause detection issues on major WAFs.
  • CPU pinning. pin_cpu_cores=True makes measurable parallelism match navigator.hardwareConcurrency, at a CPU cost, and serializes concurrent launches.
  • Containers. Run camoufox fetch in the image build, use headless=True or "virtual" with Xvfb, and pin the package and browser build so rebuilds do not silently change fingerprints.
  • Other languages. launch_server() or camoufox server exposes a Playwright server for firefox.connect(); persistent contexts cannot be served.

Camoufox vs Playwright Chromium, undetected-chromedriver and Anti-Detect Browsers

Different tools for different jobs
ToolEngine and controlHow identity is changedBest fit
CamoufoxPatched Firefox, Playwright over JugglerC++ patches; drawn, coherence-checked identity; geoipCode-first scraping and agents that can run as Firefox
Playwright Chromium (+ stealth plugin)Chromium over CDPHost’s real values, plus JS overrides if a plugin is usedSites that expect Chrome; lowest setup friction
undetected-chromedriver-style toolsInstalled Chrome via patched driver or CDPRemoves driver traces; the device is the host’s ownExisting Selenium code bases
Anti-detect browsersCommercial apps, mostly Chromium-basedVendor-managed profiles; GUI first, some local APIsTeams running persistent profiles by hand

If you need Chrome, use Playwright Chromium and keep the setup honest; our Python scraping with residential proxies and Selenium ChromeDriver proxy guides cover those stacks. For many coherent desktop identities from code, where Firefox is acceptable, Camoufox is the more complete option. For humans operating long-lived profiles, a commercial tool such as those in our Vision review fits better. All of them need an exit IP that matches the identity.

Responsible Use

Stealth is not permission

Consistent browser identities are legitimate for many jobs: monitoring public prices and availability, ad and localization checks, search research, regional QA and testing your own bot defenses. They do not change what you are allowed to do.

  • Collect public data or data you are authorized to access. Do not use automation to get past logins, paywalls or access controls you have no permission to test.
  • Read the site’s terms and robots.txt, and use an official API where one exists.
  • Rate-limit per domain and back off on errors. A block is a signal to slow down, not a puzzle to solve.
  • Do not collect personal data you have no lawful basis to process.
  • Treat CAPTCHAs as a stop sign; this guide does not cover defeating them.

Anti-bot vendors protect real businesses from fraud, scalping and abuse. Traffic that is honest about its rate and purpose is the version that keeps working.

Frequently Asked Questions

Camoufox in short
What is Camoufox?

An open-source Firefox build for web scraping and AI agents. It reports a drawn, internally consistent device identity from C++ instead of JavaScript overrides, and you control it with normal Playwright code through the Python or TypeScript package.

How do I install Camoufox in Python?

On Python 3.10 or newer, run pip install -U "camoufox[geoip]", then camoufox fetch (or python -m camoufox fetch), and replace your Playwright launch with Camoufox() or AsyncCamoufox().

How do I use a proxy with Camoufox?

Pass a Playwright proxy dict such as {"server": "http://v2.proxyempire.io:5000", "username": ..., "password": ...} with geoip=True. Copy the ProxyEmpire username and password from Proxy Manager → Connection Builder.

What does geoip=True do in Camoufox?

At launch it looks up the exit IP through the proxy and sets timezone, locale, geolocation and WebRTC IP from a local GeoIP database. Pass an IP string to skip the lookup.

Should I use rotating or sticky proxies with Camoufox?

Sticky, for any geo-matched browser. With a new IP per request, the IP the launcher looked up and the IPs the page uses differ. With a custom rotation time, relaunch before the session rotates.

Is Camoufox undetectable?

No. The project says it can still be detected, because thousands of values must agree and vendors look for the one that does not. It also only presents Firefox. Verify on public test pages and keep request rates reasonable.

Does Camoufox keep the same fingerprint between runs?

Not by default. A fresh identity is drawn each launch; a persistent profile keeps cookies, not the device. Pass the same preset with fingerprint_preset= and keep the same sticky session.

Give every Camoufox profile a matching IP

ProxyEmpire residential, mobile and datacenter proxies on one gateway, with country, city and ISP targeting and sticky sessions you control from the Connection Builder.

ProxyEmpire proxy types and prices

ProxyEmpire

Proxies for data collection at scale.

Residential, mobile, ISP and datacenter proxies for scraping, ad verification, SEO monitoring and multi-account work. Choose from 30M+ ethically sourced residential IPs and 4M+ 4G/5G mobile IPs in 170+ countries, target by country, city, ZIP, ISP or carrier, and connect over HTTP(S) or SOCKS5 with 99.9% uptime. Try residential and mobile proxies for $1.97.

🏘️ Rotating Residential Proxies
  • 30M+ Premium Residential IPs
  •  170+ Countries
    Every residential IP in our network corresponds to an actual desktop device with a precise geographical location. Our residential proxies are fast and reliable, with 99.9% uptime, and work for a wide range of use cases. You can use country, region, city, ZIP, ISP, ASN and OS fingerprint targeting for our rotating residential proxies, at no extra charge.

See our Rotating Residential Proxies

📍 Static Residential Proxies
  • 24 Countries
    Buy a dedicated static residential IP from one of the 24 countries that we offer proxies in. Keep the same IP for a month or longer, while benefiting from their fast speed and stability.

See our Static Residential Proxies

📳 Rotating Mobile Proxies
  • 4M+ Premium Mobile IPs
  •  170+ Countries
    Access millions of clean mobile IPs with precise targeting by country, region, city, ZIP, mobile carrier, ASN and OS fingerprint, at no extra charge. Get far fewer IP blocks and CAPTCHAs with our 4G and 5G proxies.

See our Mobile Proxies

📱 Dedicated Mobile Proxies
  • 4 Countries
  • 50+ Locations
    Get your own dedicated mobile proxy in one of our supported locations, with unlimited bandwidth and unlimited IP changes on demand. A great choice when you need a small number of mobile IPs and a lot of proxy bandwidth.

See our 4G & 5G Proxies

🌐 Rotating Datacenter Proxies
  • 197,000+ IPs Premium IPs
  •  62 Countries
    On a budget and need to do some simple scraping tasks? Our datacenter proxies are the perfect fit! Get started with as little as $2

See our Datacenter Proxies

♾️ Unlimited Residential Proxies
  • 1M+ Daily Residential IPs
  • 85+ Countries
    Our unlimited residential proxies bill by connection speed, not gigabytes: 100 to 1000 Mbit with unlimited bandwidth, on daily, weekly or monthly plans. Ask us for a 1-hour trial at 50 Mbit before you pick a plan.

See our Unlimited Residential Proxies

proxy locations

30M+ rotating IPs

99.9% uptime - high speed

99.9% uptime.

dedicated support team

24/7 Dedicated Support.

fair price

Fair Pricing.

🏠 Residential Proxies Rotating / Static / Unlimited
📱 Mobile Proxies Rotating and Dedicated
🖥️ Datacenter Proxies Rotating
🌍 IP Pool 30M+ residential + 4M+ mobile IPs
📶 Uptime 99.9% · Live status
💳 Payment Card · PayPal · Crypto · Bank transfer
💬 Support 24/7 live chat · [email protected]