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()orAsyncCamoufox(). geoip=Truelooks 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 launcherCamoufox 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.
camoufox package draws a device identity, checks that its parts agree, passes it to the browser and returns normal Playwright objects.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 spoofedMost 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-AgentandAccept-Languageheaders must be set separately fromnavigator. - 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 missThe 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.
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 codeThe 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.
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 runs | Notes |
|---|---|---|
| False (default) | A normal window | Needs a display |
| True | Firefox headless | Camoufox patches the headless tells it knows about |
| “virtual” | Headed browser on a private Xvfb display | Linux only; needs Xvfb; software rendering |
| “virtual-gpu” | Headed browser on a headless Xorg bound to one GPU | Linux 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 itA 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.
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.
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 putThis 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 mode | What geoip sees vs the page | Use with Camoufox? |
|---|---|---|
| New IP every request | The lookup gets one IP, page requests get others | Not with geoip; WebRTC IP and possibly timezone won’t match |
| Same IP (sticky) | Same IP for the lookup and the page | Yes, the default choice |
| Custom rotation (up to 2 h) | Matches until the session rotates | Yes, 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:
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.
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 workEvery 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.
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.
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
// 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 IPCamoufox’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.
| Option | What it controls | Consistency note |
|---|---|---|
| os= | OS of the drawn identity, or a list to pick from | Another OS than the host affects canvas (see below) |
| screen= | Bounds for the generated screen | Prefer bounds over a fixed window= |
| locale= | Locale(s); first is used for Intl | Leave unset with geoip to match the region |
| geoip= | Timezone, locale, geolocation, WebRTC IP | Needs a stable exit IP |
| humanize= | Human-like cursor paths | True or a max seconds per move |
| block_images= | Skips image requests | Saves bandwidth; warns it can trip some WAFs |
| addons= | Extracted Firefox addon folders | uBlock 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 configuredBefore 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.
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=Truerestores them, and the docs notego_back()/go_forward()depend on it. - Bandwidth. uBlock Origin blocks ad and tracker requests by default.
block_images=Truesaves more, but Camoufox warns it has been reported to cause detection issues on major WAFs. - CPU pinning.
pin_cpu_cores=Truemakes measurable parallelism matchnavigator.hardwareConcurrency, at a CPU cost, and serializes concurrent launches. - Containers. Run
camoufox fetchin the image build, useheadless=Trueor"virtual"with Xvfb, and pin the package and browser build so rebuilds do not silently change fingerprints. - Other languages.
launch_server()orcamoufox serverexposes a Playwright server forfirefox.connect(); persistent contexts cannot be served.
Camoufox vs Playwright Chromium, undetected-chromedriver and Anti-Detect Browsers
Different tools for different jobs| Tool | Engine and control | How identity is changed | Best fit |
|---|---|---|---|
| Camoufox | Patched Firefox, Playwright over Juggler | C++ patches; drawn, coherence-checked identity; geoip | Code-first scraping and agents that can run as Firefox |
| Playwright Chromium (+ stealth plugin) | Chromium over CDP | Host’s real values, plus JS overrides if a plugin is used | Sites that expect Chrome; lowest setup friction |
| undetected-chromedriver-style tools | Installed Chrome via patched driver or CDP | Removes driver traces; the device is the host’s own | Existing Selenium code bases |
| Anti-detect browsers | Commercial apps, mostly Chromium-based | Vendor-managed profiles; GUI first, some local APIs | Teams 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 permissionConsistent 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 shortWhat 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.



