Running Selenium Multiple Browser Profiles in Parallel
To run Selenium multiple browser profiles simultaneously, create separate Chrome --user-data-dir paths or Firefox profile directories for each session, then launch each WebDriver instance with its own options object. This gives every session its own cookies, localStorage, and login state — but it does not isolate browser fingerprints. For fingerprint isolation you need a browser profile manager like Sendwin Browser, which gives each profile its own consistent fingerprint settings along with separate cookies and storage. Below we cover both approaches with production-ready Python code.
Why You Need Separate Browser Profiles
Running multiple Selenium sessions against a single default profile causes chaos. Cookies collide, sessions overwrite each other, and any saved credentials bleed across tabs. Worse, every session shares the same fingerprint — which means websites can trivially link your accounts.
Common Failure Modes
- Cookie contamination — Session A’s login token overwrites Session B’s, logging one user out
- Cache conflicts — Shared cache directories cause file-lock errors under concurrent writes
- Fingerprint correlation — Identical canvas hashes, WebGL renderers, and font lists across sessions let servers link accounts to the same machine
- Rate-limit amplification — Multiple sessions from one IP with one fingerprint trigger aggressive bot detection
Separate browser profiles solve the first two problems. Solving the fingerprint problem requires additional tooling, which we address later in this guide.
Chrome: Using --user-data-dir for Profile Isolation
Chrome stores all profile data — cookies, history, extensions, cached credentials — inside a user data directory. By pointing each Selenium session at a different directory, you get full data isolation between sessions.
Basic Setup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
import os
import tempfile
def create_chrome_profile(profile_name: str) -> webdriver.Chrome:
"""Launch a Chrome instance with an isolated user data directory."""
profile_path = os.path.join(tempfile.gettempdir(), "selenium_profiles", profile_name)
os.makedirs(profile_path, exist_ok=True)
options = Options()
options.add_argument(f"--user-data-dir={profile_path}")
options.add_argument("--no-first-run")
options.add_argument("--no-default-browser-check")
options.add_argument("--disable-blink-features=AutomationControlled")
driver = webdriver.Chrome(options=options)
return driver
# Launch two isolated sessions
driver_a = create_chrome_profile("account_alpha")
driver_b = create_chrome_profile("account_beta")
driver_a.get("https://example.com/login")
driver_b.get("https://example.com/login")
Each profile directory persists between runs, so cookies and saved logins carry over — handy for maintaining logged-in sessions across automation runs.
Key Chrome Options for Multi-Profile Selenium
| Option | Purpose |
|---|---|
--user-data-dir |
Sets the profile storage directory; must be unique per session |
--profile-directory |
Selects a named sub-profile inside the user data dir (e.g., “Profile 1”) |
--no-first-run |
Skips the “Welcome to Chrome” flow on new profiles |
--disable-blink-features=AutomationControlled |
Removes the navigator.webdriver flag |
--disable-extensions |
Prevents extension conflicts between profiles |
Firefox: Managing Named Profiles
Firefox uses a profile manager with named profiles stored in profiles.ini. Selenium can target specific profiles or create ephemeral ones.
Using Existing Firefox Profiles
from selenium import webdriver
from selenium.webdriver.firefox.options import Options as FirefoxOptions
def create_firefox_profile(profile_path: str) -> webdriver.Firefox:
"""Launch Firefox with a specific profile directory."""
options = FirefoxOptions()
options.add_argument("-profile")
options.add_argument(profile_path)
# Stealth preferences
options.set_preference("dom.webdriver.enabled", False)
options.set_preference("useAutomationExtension", False)
driver = webdriver.Firefox(options=options)
return driver
# Point to two different Firefox profile directories
driver_one = create_firefox_profile("/path/to/profiles/session_one")
driver_two = create_firefox_profile("/path/to/profiles/session_two")
Creating Profiles Programmatically
import shutil
import tempfile
from selenium import webdriver
from selenium.webdriver.firefox.options import Options as FirefoxOptions
def create_temp_firefox_profile() -> tuple[webdriver.Firefox, str]:
"""Create a disposable Firefox profile for one-time sessions."""
profile_dir = tempfile.mkdtemp(prefix="fx_profile_")
options = FirefoxOptions()
options.add_argument("-profile")
options.add_argument(profile_dir)
options.set_preference("browser.shell.checkDefaultBrowser", False)
driver = webdriver.Firefox(options=options)
return driver, profile_dir
# Spawn and cleanup
driver, temp_dir = create_temp_firefox_profile()
try:
driver.get("https://example.com")
finally:
driver.quit()
shutil.rmtree(temp_dir, ignore_errors=True)
Running Profiles in Parallel With Threading
Sequential profile launches are slow. For real automation workloads — managing multiple accounts, running session isolation tests, or scraping at scale — you need concurrent sessions.
Thread-Based Parallel Sessions
import concurrent.futures
import os
import tempfile
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import WebDriverException
def run_session(profile_name: str, url: str, task_id: int) -> dict:
"""Run an isolated browser session with error handling."""
profile_path = os.path.join(
tempfile.gettempdir(), "selenium_profiles", profile_name
)
os.makedirs(profile_path, exist_ok=True)
options = Options()
options.add_argument(f"--user-data-dir={profile_path}")
options.add_argument("--no-first-run")
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")
driver = None
try:
driver = webdriver.Chrome(options=options)
driver.set_page_load_timeout(30)
driver.get(url)
title = driver.title
return {
"task_id": task_id,
"profile": profile_name,
"status": "success",
"title": title,
}
except WebDriverException as e:
return {
"task_id": task_id,
"profile": profile_name,
"status": "error",
"error": str(e),
}
finally:
if driver:
driver.quit()
# Define parallel tasks
tasks = [
("profile_shop_us", "https://shop.example.com/us", 1),
("profile_shop_uk", "https://shop.example.com/uk", 2),
("profile_shop_de", "https://shop.example.com/de", 3),
("profile_shop_jp", "https://shop.example.com/jp", 4),
]
# Execute in parallel
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
futures = {
executor.submit(run_session, name, url, tid): name
for name, url, tid in tasks
}
for future in concurrent.futures.as_completed(futures):
result = future.result()
print(f"[{result['profile']}] {result['status']}: {result.get('title', result.get('error'))}")
Process-Based Parallelism (Heavier but Safer)
Threads share memory, which can cause issues with some WebDriver versions. For maximum stability, use ProcessPoolExecutor instead:
import concurrent.futures
# Same run_session function as above
if __name__ == "__main__":
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
futures = {
executor.submit(run_session, name, url, tid): name
for name, url, tid in tasks
}
for future in concurrent.futures.as_completed(futures):
result = future.result()
print(f"[{result['profile']}] {result['status']}")
The if __name__ == "__main__" guard is mandatory on Windows to prevent recursive process spawning.
Why Standard Profiles Still Leak Your Fingerprint
Separate --user-data-dir paths isolate cookies and storage, but they do nothing about the hardware and software fingerprint that every browser session broadcasts. Two profiles launched from the same machine share:
- Canvas fingerprint — The exact same pixel-level rendering output from your GPU
- WebGL renderer string — Your GPU model reported verbatim (e.g., “ANGLE (NVIDIA GeForce RTX 4090)”)
- Screen resolution and color depth — Identical values across all sessions
- Installed fonts — Same system font list fingerprinted via JavaScript
- AudioContext fingerprint — Identical audio processing characteristics
- Navigator properties — Same
platform,hardwareConcurrency,deviceMemory
Sophisticated anti-fraud systems like Akamai, PerimeterX, and DataDome cross-reference these signals. Even if your cookies are separate, matching fingerprints across sessions flag your accounts as linked. For a deep dive into what gets leaked, see our guide on Selenium browser fingerprinting.
What Detection Systems Actually Check
| Signal | Standard Profile | Antidetect Profile |
|---|---|---|
| Cookies / localStorage | ✅ Isolated | ✅ Isolated |
| Canvas hash | ❌ Same | ✅ Unique per profile |
| WebGL renderer | ❌ Same | ✅ Spoofed per profile |
| User-Agent | ❌ Same | ✅ Unique per profile |
| Screen resolution | ❌ Same | ✅ Varied per profile |
| Timezone / locale | ❌ Same | ✅ Matched to proxy geo |
| Font list | ❌ Same | ✅ Randomized subset |
navigator.webdriver |
⚠️ Patchable | ✅ Removed natively |
Websites also check for WebGL fingerprinting patterns that remain constant no matter how many Chrome profiles you create on the same hardware.
Connecting Selenium to Antidetect Browser Profiles
Antidetect browsers solve the fingerprint problem by generating unique browser environments for each profile — different canvas noise, WebGL spoofing, font subsets, timezone, and locale. Instead of launching Chrome directly, you launch a profile in the antidetect browser and connect Selenium to its local debugging port.
The Architecture
- Create profiles in the browser profile manager (e.g., Sendwin Browser), each with its own fingerprint settings and optional proxy
- Launch a profile — the antidetect browser opens a Chromium instance on a local debugging port
- Connect Selenium via
debuggerAddressto the running instance - Automate normally — Selenium controls the browser, but all fingerprint spoofing is handled at the browser level
Connecting to a Running Antidetect Profile
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def connect_to_antidetect_profile(debug_port: int) -> webdriver.Chrome:
"""Connect Selenium to an antidetect browser profile via its debug port."""
options = Options()
options.debugger_address = f"127.0.0.1:{debug_port}"
driver = webdriver.Chrome(options=options)
return driver
# Profile A is running on port 9222, Profile B on port 9223
driver_profile_a = connect_to_antidetect_profile(9222)
driver_profile_b = connect_to_antidetect_profile(9223)
# Each session has a completely different fingerprint
driver_profile_a.get("https://browserleaks.com/canvas")
driver_profile_b.get("https://browserleaks.com/canvas")
Send.win does not offer an automation interface: there is no API, SDK, headless mode or Selenium/Puppeteer/Playwright support on any plan. You launch profiles through Sendwin Browser’s desktop app, where each profile keeps its own cookies, storage, sessions and fingerprint settings. Paid plans are Pro at $19/month or $83.88/year, and Team at $49/month or $251.88/year, with the first card purchase covered by a 7-day money-back guarantee.
Parallel Antidetect Sessions
Error Handling for Concurrent Sessions
Multi-profile Selenium setups introduce failure modes that single-session scripts never encounter. Here is a robust error-handling pattern for production use:
import logging
import os
import tempfile
import time
from contextlib import contextmanager
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import (
WebDriverException,
TimeoutException,
SessionNotCreatedException,
InvalidSessionIdException,
)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
MAX_RETRIES = 3
RETRY_DELAY = 5 # seconds
@contextmanager
def managed_driver(profile_name: str):
"""Context manager for safe WebDriver lifecycle management."""
profile_path = os.path.join(
tempfile.gettempdir(), "selenium_profiles", profile_name
)
os.makedirs(profile_path, exist_ok=True)
options = Options()
options.add_argument(f"--user-data-dir={profile_path}")
options.add_argument("--no-first-run")
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")
options.page_load_strategy = "normal"
driver = None
try:
driver = webdriver.Chrome(options=options)
driver.set_page_load_timeout(30)
driver.set_script_timeout(30)
yield driver
except SessionNotCreatedException as e:
logger.error(f"[{profile_name}] Failed to create session: {e}")
raise
finally:
if driver:
try:
driver.quit()
except Exception:
logger.warning(f"[{profile_name}] Driver quit failed — process may be orphaned")
def execute_with_retry(profile_name: str, url: str, action_fn) -> dict:
"""Execute a browser action with retry logic."""
for attempt in range(1, MAX_RETRIES + 1):
try:
with managed_driver(profile_name) as driver:
driver.get(url)
result = action_fn(driver)
return {"profile": profile_name, "status": "success", "data": result}
except TimeoutException:
logger.warning(
f"[{profile_name}] Timeout on attempt {attempt}/{MAX_RETRIES}"
)
except InvalidSessionIdException:
logger.warning(
f"[{profile_name}] Session died on attempt {attempt}/{MAX_RETRIES}"
)
except WebDriverException as e:
logger.error(
f"[{profile_name}] WebDriver error on attempt {attempt}/{MAX_RETRIES}: {e}"
)
if attempt < MAX_RETRIES:
time.sleep(RETRY_DELAY * attempt) # Exponential-ish backoff
return {"profile": profile_name, "status": "failed", "data": None}
# Usage
def scrape_title(driver):
return {"title": driver.title, "url": driver.current_url}
result = execute_with_retry("profile_alpha", "https://example.com", scrape_title)
print(result)
Common Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
SessionNotCreatedException: user data directory is already in use |
Two sessions point at the same --user-data-dir |
Ensure each profile has a unique directory path |
InvalidSessionIdException |
Browser crashed or was killed externally | Wrap in retry logic; check available RAM |
TimeoutException on page load |
Network issue or slow proxy | Increase set_page_load_timeout(); verify proxy connectivity |
DevToolsActivePort file doesn't exist |
Chrome failed to start (often a permissions issue) | Add --no-sandbox and --disable-dev-shm-usage |
| Profiles suddenly share cookies | Reused the same --user-data-dir path |
Audit profile path generation; add unique suffixes |
Scaling Beyond 10 Profiles
Running dozens of concurrent Selenium profiles on a single machine hits practical limits fast. Each Chrome instance consumes 200-500 MB of RAM and competes for CPU time. Strategies for scaling:
Resource Management
- Headless mode — Add
--headless=newto cut RAM usage by roughly 30% - Disable images — Use
prefs["profile.managed_default_content_settings.images"] = 2to skip image loading - Limit tabs — One tab per profile; avoid opening secondary tabs
- Stagger launches — Start profiles 2-3 seconds apart to avoid CPU spikes during initialization
Using Selenium Grid
For 20+ concurrent profiles, distribute sessions across multiple machines using Selenium Grid:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def create_grid_session(hub_url: str, profile_id: str) -> webdriver.Remote:
"""Create a remote session on Selenium Grid."""
options = Options()
options.add_argument("--no-first-run")
options.add_argument("--disable-blink-features=AutomationControlled")
driver = webdriver.Remote(
command_executor=hub_url,
options=options,
)
return driver
hub = "http://selenium-grid.internal:4444"
drivers = [create_grid_session(hub, f"profile_{i}") for i in range(20)]
However, Selenium Grid does not solve the fingerprint problem. Every node still runs stock Chrome with identical fingerprints. To bypass anti-bot detection at scale, you need per-session fingerprint spoofing that native Chrome cannot provide.
Cloud Browser Sessions as an Alternative
Send.win offers cloud browser sessions that run profiles on Send.win’s server in the EU with no local install required. Each cloud session gets its own fingerprint settings, proxy, and isolated environment. Send.win has no automation interface, so Selenium cannot connect to cloud-hosted profiles. Paid plans are Pro at $19/month or $83.88/year, and Team at $49/month or $251.88/year, with the first card purchase covered by a 7-day money-back guarantee. The only free option is the cloud demo at https://send.win/demo-browser/.
🏆 Send.win Verdict
Standard Selenium profiles isolate cookies and storage but share the same browser fingerprint. Sendwin Browser is a browser profile manager that gives each profile its own consistent fingerprint settings — canvas, WebGL, fonts and timezone — along with separate cookies and storage. Send.win has no automation interface, so Selenium, Puppeteer and Playwright cannot connect to its profiles. Pro costs $19/month or $83.88/year, and Team costs $49/month or $251.88/year, with the first card purchase covered by a 7-day money-back guarantee.
Try the Send.win cloud demo — a 3-minute preview without signup, 10 minutes a day with a free account.
Frequently Asked Questions
Can Selenium run multiple browser profiles at the same time?
Yes. Each webdriver.Chrome() or webdriver.Firefox() call launches an independent browser process. By assigning a unique --user-data-dir (Chrome) or -profile path (Firefox) to each instance, you get fully isolated sessions that can run concurrently via threading or multiprocessing.
Managing Multiple Browser Profiles With Send.win
Send.win pairs isolated, fingerprint-managed browser profiles with a cloud browser that runs on Send.win’s servers in the EU, so your work stays in profiles that keep their own separate cookies, storage and fingerprint:
- Isolated profiles – each with its own fingerprint, cookies, and storage
- Built-in residential proxies – with automatic timezone, locale, and WebRTC matching
- Desktop app for Windows, macOS & Linux – plus cloud sessions when you don’t want a local install
Try the instant cloud browser demo — no install, straight from your browser. Then compare plans: paid plans from $19/month, and the first card purchase is covered by a 7-day money-back guarantee.
How many Selenium profiles can I run on one machine?
It depends on available RAM and CPU. Each headed Chrome instance uses 200-500 MB of RAM. A machine with 16 GB of RAM can typically handle 15-25 concurrent profiles. Headless mode and disabling image loading can push that to 30-40. Beyond that, use Selenium Grid or cloud browser sessions.
Do separate Chrome user data directories prevent fingerprinting?
No. Separate --user-data-dir paths isolate cookies, localStorage, and cached data, but the browser fingerprint — canvas hash, WebGL renderer, screen resolution, installed fonts — remains identical across all profiles on the same machine. Anti-fraud systems use these signals to link accounts.
What is the difference between --user-data-dir and --profile-directory?
--user-data-dir sets the root storage path for the entire Chrome instance. --profile-directory selects a named sub-profile within that root (e.g., “Default”, “Profile 1”). For Selenium isolation, always use separate --user-data-dir values — sharing a root and switching sub-profiles can cause lock conflicts.
How do I connect Selenium to an antidetect browser profile?
Launch the profile in the browser (which opens a Chromium instance on a local debug port), then connect Selenium with: options.debugger_address = "127.0.0.1:PORT". This lets Selenium control the session while the browser handles fingerprint consistency, proxy routing, and cookie isolation.
Is ThreadPoolExecutor or ProcessPoolExecutor better for parallel Selenium?
ThreadPoolExecutor is lighter and works well for I/O-bound automation tasks. ProcessPoolExecutor is safer for CPU-heavy workloads or when using WebDriver versions that are not fully thread-safe. On Windows, always include the if __name__ == "__main__" guard with ProcessPoolExecutor.
Why does Selenium throw “user data directory is already in use”?
This error occurs when two Chrome instances try to use the same --user-data-dir simultaneously. Chrome locks the directory on startup. Fix it by ensuring every concurrent session has a unique profile path — use the session ID, account name, or an incrementing index in the path.
Does Send.win offer an Automation API for Selenium?
No. Send.win has no automation interface — no API, SDK, headless mode or Selenium/Puppeteer/Playwright support on any plan. You launch a profile in Sendwin Browser (the native desktop app), where each profile keeps its own cookies, storage, sessions and fingerprint settings. Paid plans are Pro at $19/month or $83.88/year, and Team at $49/month or $251.88/year, with the first card purchase covered by a 7-day money-back guarantee. The only free option is the cloud demo at https://send.win/demo-browser/.