Selenium is the go-to automation framework for most developers who need to control browsers programmatically. But standard Selenium sessions have a problem that becomes obvious the moment you point them at any platform with basic bot detection: they announce themselves. The navigator.webdriver flag is set to true, the canvas and WebGL fingerprints are generic and inconsistent, and the browser profile is blank on every run. Any detection system worth its salt will flag the session within seconds.
The typical workarounds — patching ChromeDriver, injecting JavaScript to override navigator.webdriver, or using stealth libraries — are a constant arms race. Platforms update their detection, the stealth patches need to catch up, and you spend more time maintaining the bypass layer than building the actual automation logic.
A more durable approach is to connect Selenium not to a fresh ChromeDriver instance, but to an existing browser profile that already carries a realistic, persistent fingerprint. That is exactly what the BitBrowser local API enables. Instead of launching a new browser, Selenium attaches to a running BitBrowser profile via its remote debugging port — inheriting all the fingerprint settings, cookies, and session state that profile carries.
This guide covers the full technical setup: how the connection works, the Python implementation, and the practical patterns that make this approach stable at scale.
How BitBrowser's Local API Works
BitBrowser runs a local HTTP server on port 54345 by default. This API exposes endpoints for managing browser profiles programmatically — creating, opening, closing, and querying profiles from external scripts.
When you open a profile through the API, BitBrowser launches a Chromium instance for that profile and returns a webSocketDebuggerUrl — the remote debugging address that Chrome DevTools Protocol uses. Selenium's webdriver.Chrome can attach to an existing Chrome session using this address via ChromeOptions.debugger_address.
The flow looks like this:
- Your script calls the BitBrowser API to open a specific profile
- BitBrowser launches the profile and returns the debugger port
- Selenium connects to that port using
ChromeOptions - You now control a live session with the profile's full fingerprint context
The key difference from a standard webdriver.Chrome() call is that you are not creating a browser — you are attaching to one that already exists. The fingerprint was set when the profile was configured in BitBrowser. Selenium just drives the session.
Python Implementation
Install the required dependencies first:
pip install selenium requests
Here is the base connection function:
import requests
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
BITBROWSER_API = "http://127.0.0.1:54345"
def open_profile(profile_id: str) -> str:
"""Open a BitBrowser profile and return the debugger address."""
response = requests.post(
f"{BITBROWSER_API}/browser/open",
json={"id": profile_id}
)
data = response.json()
if data.get("success"):
# Returns ws://127.0.0.1:{port}/devtools/browser/{id}
debugger_url = data["data"]["ws"]["selenium"]
return debugger_url
else:
raise Exception(f"Failed to open profile: {data.get('msg')}")
def get_driver(debugger_address: str) -> webdriver.Chrome:
"""Attach Selenium to the running BitBrowser profile."""
options = Options()
# Strip the ws:// prefix — Chrome options expects host:port format
address = debugger_address.replace("ws://", "").split("/")[0]
options.add_experimental_option("debuggerAddress", address)
# Point to BitBrowser's bundled ChromeDriver
service = Service(executable_path=r"C:\Program Files\BitBrowser\chrome\chromedriver.exe")
driver = webdriver.Chrome(service=service, options=options)
return driver
def close_profile(profile_id: str):
"""Close the profile gracefully after automation."""
requests.post(
f"{BITBROWSER_API}/browser/close",
json={"id": profile_id}
)
A full automation session using these helpers:
def run_automation(profile_id: str):
debugger_url = open_profile(profile_id)
time.sleep(2) # Allow profile to fully initialize
driver = get_driver(debugger_url)
try:
driver.get("https://example.com")
# Verify the fingerprint is active — webdriver flag should be absent
wd_flag = driver.execute_script("return navigator.webdriver")
print(f"navigator.webdriver: {wd_flag}") # Should return None or False
# Your automation logic here
title = driver.title
print(f"Page title: {title}")
finally:
# Do NOT call driver.quit() — that would close the profile
# Just detach and close via the API
close_profile(profile_id)
if __name__ == "__main__":
run_automation("your-profile-id-here")
Note the driver.quit() warning in the code. Calling quit() on a Selenium session connected to BitBrowser will close the underlying Chromium process, which can corrupt the profile's state. Always close via the BitBrowser API instead.
Getting Profile IDs Programmatically
Rather than hardcoding profile IDs, you can query the list endpoint to select profiles dynamically:
def list_profiles(page: int = 0, page_size: int = 10) -> list:
"""Fetch available profiles from BitBrowser."""
response = requests.post(
f"{BITBROWSER_API}/browser/list",
json={"page": page, "pageSize": page_size}
)
data = response.json()
if data.get("success"):
return data["data"]["list"]
return []
def get_profile_by_name(name: str) -> dict | None:
"""Find a specific profile by its display name."""
profiles = list_profiles(page_size=50)
for profile in profiles:
if profile.get("name") == name:
return profile
return None
# Usage
profile = get_profile_by_name("LinkedIn - Client A")
if profile:
run_automation(profile["id"])
This is useful for multi-profile workflows where you cycle through accounts sequentially — each profile carries its own persistent identity, and your automation logic stays generic.
Running Multiple Profiles Concurrently
For parallel automation across several profiles, use Python's concurrent.futures:
from concurrent.futures import ThreadPoolExecutor, as_completed
def process_profile(profile_id: str) -> dict:
"""Wrapper that returns results per profile."""
try:
debugger_url = open_profile(profile_id)
time.sleep(2)
driver = get_driver(debugger_url)
driver.get("https://target-site.com/dashboard")
result = driver.find_element("css selector", ".metric-value").text
close_profile(profile_id)
return {"profile_id": profile_id, "result": result, "status": "ok"}
except Exception as e:
return {"profile_id": profile_id, "error": str(e), "status": "failed"}
profile_ids = ["id_001", "id_002", "id_003", "id_004", "id_005"]
with ThreadPoolExecutor(max_workers=3) as executor:
futures = {executor.submit(process_profile, pid): pid for pid in profile_ids}
for future in as_completed(futures):
output = future.result()
print(output)
Keep max_workers at 3 to 5 for most workloads. Each open profile consumes a Chromium process — running 20 simultaneously on a standard machine will exhaust RAM and cause unstable behavior. For high-volume parallel automation, BitBrowser's cloud phone feature offloads mobile profile execution to remote infrastructure, which removes the local resource constraint entirely.
Why This Approach Beats Stealth Patching
Standard Selenium stealth approaches work by patching the browser after it launches — injecting scripts that override navigator.webdriver, spoofing canvas responses, and removing automation-specific Chrome flags. The problem is that these patches operate at the JavaScript layer, and detection systems that run before your scripts execute — at the TLS handshake level, or through browser binary fingerprinting — are not affected by them at all.
BitBrowser's fingerprinting operates at the browser engine level, not through JavaScript injection. Canvas noise is applied in the rendering pipeline. WebGL parameters are spoofed at the driver level. navigator.webdriver is absent because the browser was not launched by a standard WebDriver initialization — Selenium is connecting to a session that was already running.
The result is that checks which reliably catch stealth-patched Selenium — like CreepJS or Cloudflare's bot management — behave differently against a properly configured BitBrowser session. The signals are consistent because they come from the profile's persistent configuration, not from per-session injection.
Practical Use Cases
The BitBrowser + Selenium combination fits a specific category of automation work: scenarios where session persistence, fingerprint consistency, and proxy isolation matter more than raw scraping speed.
Account warming and management — automating gradual engagement activity on freshly created accounts before running outreach or campaigns. The profile maintains session history between runs.
Multi-account testing — QA workflows where you need to verify how a feature behaves for users in different regions, device types, or account tiers. Each BitBrowser profile represents a distinct user environment.
Competitor and market monitoring — scraping platforms that use behavioral fingerprinting to detect and block scrapers. A persistent profile with a residential proxy and realistic session history bypasses most of these systems.
Affiliate and referral link testing — verifying that tracking links, landing pages, and conversion flows work correctly across multiple independent browser environments. Isolated profiles ensure there is no cross-contamination between test sessions.
BitBrowser supports up to hundreds of profiles depending on the subscription tier, making it practical for both small teams running a handful of accounts and larger operations cycling through dozens of profiles in automated workflows.
Stability Considerations
A few patterns that make this setup reliable in production rather than just in development:
Always add a time.sleep() after calling open_profile before attempting to connect with Selenium. The profile needs a moment to fully initialize the Chromium process and expose the debugger port. Two seconds is usually sufficient; on slower machines, increase to four.
Handle the case where a profile is already open. The BitBrowser API will return the existing debugger address if the profile is already running — your code should check for this rather than treating it as an error.
Rotate profile usage to avoid behavioral patterns. If you are using profiles for account management, vary the time between sessions and the sequence of actions. The fingerprint isolation handles the device identity; you still need to avoid robotic behavioral patterns at the application layer.
Keep BitBrowser and its bundled ChromeDriver version in sync. Selenium will throw version mismatch errors if your ChromeDriver is not compatible with the Chromium version BitBrowser uses. BitBrowser ships its own ChromeDriver — always point your Service path to that binary rather than a system-installed ChromeDriver.
Tags: #selenium #webdev #automation #security




Top comments (0)