💻AI Codingintermediate

Camoufox in Production: Why I Run a Firefox Stealth Browser, Not Playwright Chromium

I built a Camoufox-based browser engine that scores zero detection where Chromium fails twelve checks, and run it across a multi-site empire for legitimate SEO operations. Here is why CDP is the real tell, how the profile-per-platform model works, and the install gotcha that wastes your first hour.

By··7 min read·Reviewed
Camoufox Stealth Browser Python Tutorial - Beat Anti-Bot Walls

I run an engine built on Camoufox across a multi-site empire, and the single test that converted me away from Playwright Chromium was bot.sannysoft.com. Side by side, on the same box, with the same automation code: stock Chrome failed twelve of the detection checks. The Camoufox build failed zero. That gap is the whole argument, and the reason it exists is not what most stealth tutorials tell you.

The real tell is CDP, not the WebGL string

Every Chromium-based automation tool, Playwright and Puppeteer included, drives the browser over the Chrome DevTools Protocol. CDP is a remote debugging channel, and anti-bot vendors have learned to fingerprint its presence directly. You can patch navigator.webdriver, spoof your WebGL vendor, randomise your canvas, and inject a hundred stealth shims, and a sophisticated detector can still notice that a debugging protocol is attached to the page. A pile of JavaScript stealth patches plateaus for exactly this reason: you are decorating a browser that is announcing itself at the protocol layer.

Camoufox sidesteps the entire category. It is a patched build of Firefox, and Firefox does not speak CDP. The hardening happens inside the browser binary, not as a layer of JS evals a detector can catch mid-execution. My own first anti-detection stack was sixteen JavaScript patches plus behavioural layers on top of a Chromium CDP session. It worked, but the explicit next step in my build notes was "Camoufox engine, eliminates CDP protocol detection." Moving the engine off Chromium did more for the detection score than any single JS patch.

Use it for the right reasons: QA against your own bot-sensitive flows, accessibility and load testing, and data collection on platforms where your access is legitimate and you stay inside their rules. It is not a license to ignore terms, robots directives, login walls, or rate limits. I will come back to that line, because it is load-bearing.

Install on Python 3.12, including the binary fetch

Camoufox is two things: a Python package, and a patched Firefox binary it manages separately. The most common wasted hour is installing the package and forgetting the binary, then watching the first launch fail with no browser to drive.

python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install "camoufox[geoip]"
camoufox fetch
python -m pip freeze | grep -i camoufox

The [geoip] extra pulls a GeoIP database so the browser can correlate its profile against proxy geography, which matters the moment you route through an exit node in another country. The camoufox fetch line is the one people skip: it downloads the patched Firefox build into a local cache, and without it the package has nothing to launch. The engine I run was pinned at Camoufox 0.4.11 against browser 135.0.1-beta.24. Pin yours the same way after testing, because both the wrapper and the underlying Firefox move quickly:

python -m pip install "camoufox[geoip]==0.4.11"

On a modern Ubuntu box you will hit PEP 668 if you try to install against system Python, which returns error: externally-managed-environment. Use the venv as shown. Do not reach for --break-system-packages. The Python environment traps on Ubuntu 24.04 cover this error and the stdlib name-collision bug that bites the same projects.

First sanity check, against a fingerprint page only

Confirm the install works against a harmless target before you point it anywhere real. You are separating browser setup bugs from target-site behaviour, and that separation saves hours later.

from pathlib import Path

from camoufox.sync_api import Camoufox

def main() -> None:
    with Camoufox(headless=True) as browser:
        page = browser.new_page()
        page.goto("https://bot.sannysoft.com", wait_until="domcontentloaded")
        Path("detection-check.png").write_bytes(page.screenshot(full_page=True))
        print(page.title())

if __name__ == "__main__":
    main()

bot.sannysoft.com is the page I benchmark against because it renders a clear pass or fail per check. Screenshot it on a fresh install, screenshot it after every Camoufox upgrade, and keep the images. A stealth browser is a dependency, and dependencies regress.

The async API is the one you actually ship

The sync API is fine for a sanity check. Real pipelines are async, because you are feeding a queue or a worker, not running a script by hand.

import asyncio

from camoufox.async_api import Camoufox

async def main() -> None:
    async with Camoufox(headless=True) as browser:
        page = await browser.new_page()
        await page.goto("https://example.com", wait_until="domcontentloaded")
        print(await page.title())

asyncio.run(main())

The wrapper I run is an async Camoufox helper with one rule baked in: a persistent profile per platform, never one shared profile across everything. This is the gotcha that bites people graduating from one-off scripts to running across many sites. If a single browser identity touches twenty platforms, those platforms can correlate the same fingerprint across all of them, linking activity that should stay separate. One persistent profile per platform keeps each session internally consistent over time, which reads far more human than a fresh random fingerprint on every launch. Randomising everything every time is itself a signal.

What to verify in the fingerprint

Camoufox aims for internal consistency, and consistency matters more than any single spoofed value. Inspect these surfaces yourself rather than trusting fixed claims:

Surface What to inspect Production note
navigator.webdriver Must not announce automation Re-verify on every pinned version
WebGL vendor and renderer Plausible together, not a mismatched pair Do not mix random profiles per launch
Canvas Stable within a session, not identical across all sessions Watch for drift after upgrades
Fonts Match the platform profile Measure, do not assume a font count
Timezone and locale Match proxy geography Keep proxy, locale, and timezone aligned

When I repaired a fingerprint validator against CreepJS, the lesson was that the old single "trust score" gauge had been replaced by three separate ratings: headless, like-headless, and stealth, each scored zero to a hundred, plus a count of detected lies. A browser is only as clean as its worst rating, because each is a different lens on the same exposure. Score against the worst signal, not an average. An average hides the one check that gives you away.

from camoufox.sync_api import Camoufox

SCRIPT = """() => {
  const canvas = document.createElement("canvas");
  const gl = canvas.getContext("webgl");
  const ext = gl ? gl.getExtension("WEBGL_debug_renderer_info") : null;
  return {
    webdriver: navigator.webdriver,
    platform: navigator.platform,
    languages: navigator.languages,
    webglVendor: gl && ext ? gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : null,
    webglRenderer: gl && ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : null
  };
}"""

with Camoufox(headless=True) as browser:
    page = browser.new_page()
    page.goto("https://example.com")
    print(page.evaluate(SCRIPT))

Log this on every upgrade. The day a value shifts is the day to find out before a job does.

Proxies change the path, not the legitimacy

A proxy only changes your network route. If your browser profile says one country and your IP geography says another, you have made detection easier, not harder. Read credentials from the environment, fail loudly when they are missing, and rotate at the job boundary, never mid-page.

import os

from camoufox.sync_api import Camoufox

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

with Camoufox(proxy=proxy, geoip=True, headless=True) as browser:
    page = browser.new_page()
    page.goto("https://httpbin.org/ip", wait_until="domcontentloaded")
    print(page.inner_text("body"))

Passing geoip=True lets Camoufox align its locale and timezone hints to the exit IP, which is the point of installing the GeoIP extra. Keep one proxy identity per coherent session.

How I actually use it, and the line I do not cross

This is the part most tutorials cannot write, because they have not run it at scale. My links operation uses Camoufox as the browsing layer for legitimate SEO work at a velocity competitors cannot match by hand: submitting to a curated allowlist of real, moderated directories, rescuing competitor dead links with Wayback proof and a relevant replacement, reclaiming unlinked brand mentions through genuine outreach, and seeding posts in communities where I hold earned reputation. The speed is the edge, because Google's link classifier evaluates link networks relationally and in real time, so the only durable strategy is to do legitimate things faster than a manual team could.

The fence is absolute, and it is what keeps the speed safe. No private blog networks. No comment spam, forum spam, or profile-link padding. No Web 2.0 farms or link wheels. No submitting to an unvetted directory. And the hard one, stated plainly: I do not brute-force Cloudflare Turnstile, hCaptcha, or any challenge a real human would have to solve. If a person cannot pass it, my automation does not pass it either. Outreach never instructs anyone on anchor text. Stealth here means not tripping crude bot walls on platforms where I have a legitimate reason to be, not bypassing human verification. The moment a tool crosses into deceiving a person, it stops being SEO and becomes the thing that gets a whole empire of sites penalised at once.

Waiting and retries that observe, not pretend

Wait for the application state you need, not a blind sleep, so a failure is visible instead of a silently empty result.

import time

from camoufox.sync_api import Camoufox, TimeoutError

def fetch_html(url: str, attempts: int = 3) -> str:
    last_error: Exception | None = None
    for attempt in range(1, attempts + 1):
        try:
            with Camoufox(headless=True) as browser:
                page = browser.new_page()
                page.goto(url, wait_until="domcontentloaded", timeout=30_000)
                page.wait_for_selector("main", timeout=15_000)
                return page.content()
        except TimeoutError as exc:
            last_error = exc
            time.sleep(min(2 ** attempt, 10))
    raise RuntimeError(f"Failed to fetch {url} after {attempts} attempts") from last_error

Retries handle network blips and transient errors. They never hammer a target that is actively refusing you, because that is both rude and a fast route to a hard block.

Quick takeaways

  • Camoufox beats Playwright Chromium because Firefox does not speak CDP, the debugging protocol anti-bot vendors fingerprint directly. The hardening is in the binary, not patched-on JS.
  • I measured zero detection on bot.sannysoft.com where stock Chrome failed twelve checks. Benchmark it yourself and re-screenshot after every upgrade.
  • Install with pip install "camoufox[geoip]" then camoufox fetch. The binary fetch is the step people forget, and the package cannot launch without it.
  • Run one persistent profile per platform, not a shared identity, and align proxy, timezone, and locale. Randomising everything per launch is its own tell.
  • Score your fingerprint against the worst signal, never an average. Modern detectors expose multiple ratings, and the most-detected one defines your exposure.
  • The ethics are the architecture. Legitimate platforms only, no challenge brute-forcing, no spam networks. Speed inside the rules is a moat; speed outside them is a sitewide penalty.

Related