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 true fingerprint isolation you need an antidetect browser like Sendwin Browser, which assigns each profile a unique canvas hash, WebGL renderer, User-Agent, and timezone. 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 antidetect browser (e.g., Sendwin Browser), each with its own fingerprint configuration 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")
This pattern works with Send.win’s Automation API, which is available on both the Pro plan ($9.99/month, or $6.99/month annual) and the Team plan ($29.99/month, or $20.99/month annual). You launch profiles through Sendwin Browser’s desktop app, and each profile exposes a local automation endpoint that Selenium, Puppeteer, or Playwright can connect to.
Parallel Antidetect Sessions
import concurrent.futures
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import WebDriverException
def run_antidetect_session(
profile_name: str, debug_port: int, url: str
) -> dict:
"""Connect to an antidetect profile and perform a task."""
try:
options = Options()
options.debugger_address = f"127.0.0.1:{debug_port}"
driver = webdriver.Chrome(options=options)
driver.set_page_load_timeout(30)
driver.get(url)
title = driver.title
cookies = len(driver.get_cookies())
return {
"profile": profile_name,
"status": "success",
"title": title,
"cookies": cookies,
}
except WebDriverException as e:
return {
"profile": profile_name,
"status": "error",
"error": str(e),
}
# Note: Don't quit the driver — the antidetect browser manages the lifecycle
# Profiles launched in Sendwin Browser, each on its own port
profiles = [
("us_account", 9222, "https://example.com/dashboard"),
("uk_account", 9223, "https://example.com/dashboard"),
("de_account", 9224, "https://example.com/dashboard"),
]
with concurrent.futures.ThreadPoolExecutor(max_workers=len(profiles)) as executor:
futures = [
executor.submit(run_antidetect_session, name, port, url)
for name, port, url in profiles
]
for future in concurrent.futures.as_completed(futures):
result = future.result()
print(f"[{result['profile']}] {result['status']}")
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 in the cloud with no local install required. Each cloud session gets its own fingerprint, proxy, and isolated environment. For automation workloads that need to scale past local hardware limits, cloud sessions eliminate the need to manage Selenium Grid infrastructure entirely. Combined with the Automation API (available on Pro at $9.99/month or $6.99/month annual, and Team at $29.99/month or $20.99/month annual), you can connect Selenium to cloud-hosted profiles the same way you connect to local ones — via the profile’s automation endpoint.
🏆 Send.win Verdict
Standard Selenium profiles isolate cookies and storage but share the same browser fingerprint — which is the first thing modern detection systems check. Sendwin Browser gives each profile a unique canvas, WebGL, font, and timezone fingerprint at the browser level, and its Automation API lets you connect Selenium (or Puppeteer or Playwright) to those profiles with a single debugger_address line. Pro plan starts at $9.99/month ($6.99/month annual) with 150 profiles and built-in proxy bandwidth. For teams running 500+ profiles, the Team plan at $29.99/month ($20.99/month annual) adds 16 seats and 20 GB of proxy bandwidth.
Try Send.win free today — 30-day trial, no credit card, full Automation API access.
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.
Automate Selenium Multiple Browser Profiles With Send.win
Send.win pairs isolated, fingerprint-managed browser profiles with a full Automation API, so your scripts run in profiles that look and behave like real, separate users:
- Selenium, Puppeteer & Playwright support – drive any profile programmatically (Team plan)
- 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: a 30-day free trial with no credit card, and paid plans from $6.99/month billed annually.
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 antidetect 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 antidetect browser handles fingerprint spoofing, 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.
Can I use Send.win’s Automation API with Selenium?
Yes. Send.win’s Automation API supports Selenium, Puppeteer, and Playwright. You launch a profile in Sendwin Browser (the native desktop app), which exposes a local automation endpoint. Selenium connects via the standard debugger_address option. The API is available on both the Pro plan ($9.99/month, or $6.99/month annual) and Team plan ($29.99/month, or $20.99/month annual), with a 30-day free trial and no credit card required.