Getting blocked by PerimeterX feels like hitting a brick wall. You've built your scraper, tested your code, and watched it crash against the "Press & Hold" challenge within seconds.
That frustrating CAPTCHA page means PerimeterX (now called HUMAN Security) has flagged your requests as automated traffic. It's protecting sites like Zillow, Wayfair, StockX, Fiverr, and thousands of e-commerce platforms.
But PerimeterX isn't invincible. This guide shows you five proven methods to bypass PerimeterX protection with working code examples you can deploy today.
What is PerimeterX (HUMAN)?
PerimeterX is a sophisticated Web Application Firewall (WAF) that identifies and blocks automated traffic. Unlike basic bot detection, it uses machine learning algorithms to calculate a trust score for every visitor.
When your trust score drops below the threshold, you'll see the infamous "Press & Hold to confirm you are human" challenge—or worse, a flat 403 forbidden response.
PerimeterX analyzes multiple fingerprinting layers simultaneously. Your IP reputation, TLS handshake, HTTP headers, browser fingerprint, and behavioral patterns all feed into that single trust score. Miss any layer, and you're blocked.
How PerimeterX Detects Bots
Before attempting any bypass, you need to understand what you're fighting against. PerimeterX uses five primary detection vectors that work together as a unified scoring system.
TLS Fingerprinting
Every HTTPS connection starts with a TLS handshake where your client advertises its capabilities. Different HTTP libraries create distinct fingerprints called JA3/JA4 hashes.
Python's requests library produces a fingerprint that screams "automated script." Websites can identify this before your HTTP headers even arrive.
# This fingerprint is instantly recognized as a bot
import requests
response = requests.get("https://example.com") # JA3 hash: dead giveaway
Real browsers like Chrome create specific JA3 fingerprints that anti-bot systems whitelist. Your scraper needs to match these patterns exactly.
IP Reputation Analysis
PerimeterX maintains massive databases of IP addresses and their historical behavior. Datacenter IPs from AWS, Google Cloud, or DigitalOcean carry heavy negative scores.
Residential IPs from actual ISPs have higher trust. Mobile carrier IPs are even better because they're shared among many legitimate users.
High request volumes from single IPs trigger rate limiting. Geographic inconsistencies between your IP location and browser timezone raise additional flags.
HTTP Header Inspection
Your HTTP headers tell a story about who you are. PerimeterX checks header values, ordering, and completeness against known browser profiles.
Headers sent out of order, missing standard browser headers, or inconsistent User-Agent strings all reduce your trust score. The Sec-Ch-Ua client hints in modern Chrome versions are particularly scrutinized.
JavaScript Browser Fingerprinting
Once your request passes initial checks, PerimeterX injects client-side JavaScript that collects detailed browser characteristics.
This includes canvas rendering, WebGL parameters, installed fonts, screen dimensions, audio context hashes, and hundreds of other data points. The fingerprint must be internally consistent.
Headless browsers often leak detection signals through missing APIs, incorrect timing functions, or absent rendering capabilities. Standard Puppeteer and Playwright setups fail these checks instantly.
Behavioral Analysis
PerimeterX monitors your browsing patterns throughout your session. Bots typically navigate too fast, access pages in predictable sequences, skip images and CSS, and produce no mouse movements or scroll events.
Human visitors browse chaotically—they pause, scroll, backtrack, and take varying amounts of time on each page. Your scraper needs to simulate this randomness.
How to Detect PerimeterX on Websites
Before applying bypass techniques, confirm you're actually facing PerimeterX. Look for these indicators:
Cookies: Check for _px, _px2, _px3, _pxhd, or _pxvid cookies in your browser's developer tools.
Network requests: Watch for connections to collector-*.perimeterx.net or collector-*.px-cloud.net domains.
Response headers: Look for x-px-authorization headers in request traffic.
Page source: Search for references to px.js or HUMAN branding in the HTML source.
Challenge page: The "Press & Hold" button is the most obvious sign—PerimeterX's proprietary CAPTCHA.
import re
def detect_perimeterx(response_text, cookies, url):
"""Detect if a website uses PerimeterX protection"""
# Check for PX cookies
px_cookies = ['_px', '_px2', '_px3', '_pxhd', '_pxvid']
for cookie_name in px_cookies:
if cookie_name in cookies:
return True, f"Found {cookie_name} cookie"
# Check response body for PX indicators
px_patterns = [
r'perimeterx\.net',
r'px-cloud\.net',
r'human\.com.*challenge',
r'Press & Hold',
r'_pxAppId'
]
for pattern in px_patterns:
if re.search(pattern, response_text, re.IGNORECASE):
return True, f"Found pattern: {pattern}"
return False, "No PerimeterX detected"
Method 1: Use Stealth Headless Browsers (Camoufox)
Standard browser automation tools like Selenium, Playwright, and Puppeteer get detected within milliseconds. They leave fingerprint traces that PerimeterX identifies immediately.
Camoufox changes the game. It's a modified Firefox build that spoofs fingerprints at the C++ implementation level—not through JavaScript injection that anti-bots detect.
Why Camoufox Works
Most stealth tools patch browser behavior through JavaScript overrides. Anti-bot systems detect these patches by checking for inconsistencies between reported values and actual behavior.
Camoufox modifies Firefox's source code directly. When websites query browser properties, they receive authentic values that match real user traffic patterns.
The browser uses BrowserForge to rotate device fingerprints based on statistical distributions of real-world traffic. Your scraper presents a different but realistic identity for each session.
Installation
pip install camoufox[geoip]
camoufox fetch
Basic Usage
from camoufox.sync_api import Camoufox
with Camoufox(headless=True) as browser:
page = browser.new_page()
page.goto("https://perimeterx-protected-site.com")
# Page loads normally - fingerprint matches real Firefox
content = page.content()
print(content)
Advanced Configuration with Proxy Support
For sites with aggressive IP filtering, combine Camoufox with residential proxies. The geoip=True parameter automatically configures browser timezone and language to match your proxy's location.
from camoufox.sync_api import Camoufox
import time
import random
def scrape_with_camoufox(url, proxy_config=None):
"""
Scrape PerimeterX-protected site with full fingerprint spoofing
"""
config = {
"headless": True,
"os": random.choice(["windows", "macos", "linux"])
}
if proxy_config:
config["proxy"] = proxy_config
config["geoip"] = True # Auto-match timezone to proxy IP
with Camoufox(**config) as browser:
page = browser.new_page()
# Navigate with human-like timing
page.goto(url, wait_until="networkidle")
# Simulate natural browsing behavior
time.sleep(random.uniform(2, 4))
# Random scroll to trigger content loading
page.mouse.wheel(0, random.randint(300, 800))
time.sleep(random.uniform(1, 2))
# Extract page content
html = page.content()
return html
# Example with residential proxy
proxy = {
"server": "http://proxy.example.com:8080",
"username": "your_user",
"password": "your_pass"
}
result = scrape_with_camoufox(
"https://www.zillow.com/homes/for_sale/",
proxy_config=proxy
)
Key Features That Bypass PerimeterX
Fingerprint rotation: Each session presents different but realistic device characteristics. Navigator properties, screen dimensions, WebGL parameters, and fonts all rotate automatically.
Stealth patches: Fixes navigator.webdriver detection, headless Firefox markers, and automation API leaks. JavaScript executes in an isolated sandbox invisible to page scripts.
Anti-font fingerprinting: Shifts letter spacing by random sub-pixel values to prevent font metric identification.
WebRTC IP spoofing: Modifies ICE candidates at the protocol level to match your proxy IP address.
Method 2: TLS Fingerprint Impersonation (curl_cffi)
For high-volume scraping where browser automation is too resource-intensive, you need HTTP-level bypasses. The curl_cffi library impersonates browser TLS fingerprints without launching actual browsers.
How curl_cffi Works
Regular Python HTTP libraries like requests and httpx use OpenSSL configurations that produce non-browser JA3 fingerprints. Websites identify these instantly.
curl_cffi wraps curl-impersonate, a modified libcurl that mimics the exact TLS handshake of Chrome, Firefox, Safari, and Edge. Your requests become indistinguishable from real browser traffic at the network level.
Installation
pip install curl_cffi
Basic Implementation
from curl_cffi import requests
# Standard request - instantly blocked
# response = requests.get("https://protected-site.com")
# Impersonate Chrome 131 - matches real browser fingerprint
response = requests.get(
"https://protected-site.com",
impersonate="chrome131"
)
print(response.status_code)
print(response.text[:500])
Session Management with Proxy Rotation
For sustained scraping, maintain sessions with proper cookie handling and rotate proxies to distribute requests.
from curl_cffi import requests
import random
import time
class TLSBypassScraper:
"""
High-performance scraper with TLS fingerprint impersonation
"""
BROWSER_PROFILES = [
"chrome131", "chrome130", "chrome126",
"firefox128", "firefox121",
"safari17.2", "safari17.4"
]
def __init__(self, proxies=None):
self.proxies = proxies or []
self.session = None
self._create_session()
def _create_session(self):
"""Create new session with random browser profile"""
browser = random.choice(self.BROWSER_PROFILES)
self.session = requests.Session(impersonate=browser)
# Set realistic headers
self.session.headers.update({
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"DNT": "1",
"Upgrade-Insecure-Requests": "1"
})
def _get_proxy(self):
"""Get random proxy from pool"""
if not self.proxies:
return None
return random.choice(self.proxies)
def get(self, url, max_retries=3):
"""
Fetch URL with automatic retry and proxy rotation
"""
for attempt in range(max_retries):
try:
proxy = self._get_proxy()
proxies = {"http": proxy, "https": proxy} if proxy else None
response = self.session.get(
url,
proxies=proxies,
timeout=30
)
# Check for PerimeterX block
if response.status_code == 403:
if "perimeterx" in response.text.lower():
print(f"PerimeterX block detected, rotating session...")
self._create_session()
continue
if response.status_code == 200:
return response
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
time.sleep(2 ** attempt) # Exponential backoff
return None
# Usage
proxies = [
"http://user:pass@residential1.proxy.com:8080",
"http://user:pass@residential2.proxy.com:8080",
]
scraper = TLSBypassScraper(proxies=proxies)
response = scraper.get("https://www.fiverr.com")
if response:
print(f"Success: {len(response.text)} bytes")
Supported Browser Fingerprints
curl_cffi supports impersonating these browsers (as of late 2025):
| Browser | Versions Available |
|---|---|
| Chrome | 99-131 |
| Firefox | 102-128 |
| Safari | 15.3-17.4 |
| Edge | 99-127 |
Use recent browser versions. Outdated fingerprints like chrome99 are increasingly flagged by anti-bot systems.
Method 3: Undetected ChromeDriver for Selenium
If your existing codebase uses Selenium, switching frameworks isn't always practical. undetected-chromedriver patches ChromeDriver to hide automation signals while maintaining Selenium compatibility.
Installation
pip install undetected-chromedriver
Basic Usage
import undetected_chromedriver as uc
# Standard ChromeDriver gets blocked instantly
# driver = webdriver.Chrome()
# Undetected version bypasses basic detection
driver = uc.Chrome(headless=True, version_main=131)
driver.get("https://protected-site.com")
print(driver.page_source[:500])
driver.quit()
Enhanced Configuration
import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
import random
def create_stealth_driver(proxy=None):
"""
Create undetected Chrome instance with optimal settings
"""
options = uc.ChromeOptions()
# Performance optimizations
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
# Realistic browser settings
options.add_argument("--window-size=1920,1080")
options.add_argument("--lang=en-US")
# Disable automation flags
options.add_argument("--disable-blink-features=AutomationControlled")
if proxy:
options.add_argument(f"--proxy-server={proxy}")
driver = uc.Chrome(
options=options,
version_main=131, # Match your Chrome version
headless=True
)
# Additional stealth
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
"""
})
return driver
def scrape_with_behavior(driver, url):
"""
Scrape URL with human-like browsing behavior
"""
driver.get(url)
# Wait for page load
time.sleep(random.uniform(2, 4))
# Scroll behavior
scroll_amount = random.randint(300, 700)
driver.execute_script(f"window.scrollBy(0, {scroll_amount})")
time.sleep(random.uniform(1, 2))
# Random mouse movement simulation via JavaScript
driver.execute_script("""
document.dispatchEvent(new MouseEvent('mousemove', {
clientX: Math.random() * window.innerWidth,
clientY: Math.random() * window.innerHeight
}));
""")
return driver.page_source
# Usage
driver = create_stealth_driver()
try:
html = scrape_with_behavior(driver, "https://www.wayfair.com")
print(f"Scraped {len(html)} bytes")
finally:
driver.quit()
Limitations
undetected-chromedriver works well against moderate protection levels but struggles with aggressive PerimeterX implementations. The patches are open-source, so anti-bot companies study and adapt to them.
For heavily protected sites, consider Camoufox or commercial solutions instead.
Method 4: Playwright with Stealth Plugin
Playwright offers better performance than Selenium with similar stealth patching capabilities. The playwright-stealth plugin hides common automation markers.
Installation
pip install playwright playwright-stealth
playwright install chromium
Implementation
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync
import random
import time
def scrape_with_playwright_stealth(url, proxy=None):
"""
Use Playwright with stealth patches to bypass PerimeterX
"""
with sync_playwright() as p:
# Browser launch options
launch_options = {
"headless": True,
"args": [
"--disable-blink-features=AutomationControlled",
"--no-sandbox"
]
}
if proxy:
launch_options["proxy"] = {"server": proxy}
browser = p.chromium.launch(**launch_options)
# Create context with realistic viewport
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
locale="en-US",
timezone_id="America/New_York"
)
page = context.new_page()
# Apply stealth patches
stealth_sync(page)
# Navigate with human timing
page.goto(url, wait_until="networkidle")
time.sleep(random.uniform(2, 4))
# Behavioral signals
page.mouse.move(
random.randint(100, 800),
random.randint(100, 600)
)
page.mouse.wheel(0, random.randint(200, 500))
time.sleep(random.uniform(1, 2))
html = page.content()
browser.close()
return html
# Usage
content = scrape_with_playwright_stealth("https://protected-site.com")
print(f"Retrieved {len(content)} bytes")
Async Version for Better Performance
import asyncio
from playwright.async_api import async_playwright
from playwright_stealth import stealth_async
import random
async def scrape_multiple_urls(urls, max_concurrent=5):
"""
Scrape multiple URLs concurrently with stealth
"""
semaphore = asyncio.Semaphore(max_concurrent)
async def scrape_single(url):
async with semaphore:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
await stealth_async(page)
try:
await page.goto(url, wait_until="networkidle")
await asyncio.sleep(random.uniform(1, 3))
content = await page.content()
return url, content
except Exception as e:
return url, None
finally:
await browser.close()
tasks = [scrape_single(url) for url in urls]
results = await asyncio.gather(*tasks)
return dict(results)
# Usage
urls = [
"https://site1.com/page1",
"https://site1.com/page2",
"https://site1.com/page3",
]
results = asyncio.run(scrape_multiple_urls(urls))
Method 5: Session Warming and Behavioral Simulation
Even with perfect fingerprints, PerimeterX monitors your browsing patterns. Bots that jump directly to product pages or scrape sequentially get flagged quickly.
Session warming establishes trust by mimicking legitimate user journeys before accessing target pages.
The Warming Strategy
Real users don't bookmark product URLs and visit them directly. They browse homepages, search for products, and navigate through categories.
Your scraper should follow similar patterns:
- Visit the homepage first
- Browse category pages
- Use the site's search function
- Navigate to target pages through internal links
- Maintain realistic timing between requests
Implementation
from curl_cffi import requests
import random
import time
from urllib.parse import urljoin
class SessionWarmer:
"""
Warm up sessions to build trust with PerimeterX before scraping
"""
def __init__(self, base_url, proxy=None):
self.base_url = base_url
self.proxy = proxy
self.session = requests.Session(impersonate="chrome131")
if proxy:
self.session.proxies = {"http": proxy, "https": proxy}
def _random_delay(self, min_sec=1, max_sec=4):
"""Human-like delay between actions"""
time.sleep(random.uniform(min_sec, max_sec))
def visit_homepage(self):
"""Start session by visiting homepage"""
print(f"Visiting homepage: {self.base_url}")
response = self.session.get(self.base_url)
self._random_delay(2, 5)
return response.status_code == 200
def browse_random_pages(self, paths, count=3):
"""
Visit random pages to establish browsing pattern
"""
selected = random.sample(paths, min(count, len(paths)))
for path in selected:
url = urljoin(self.base_url, path)
print(f"Browsing: {url}")
try:
response = self.session.get(url)
if response.status_code != 200:
print(f"Warning: Got {response.status_code}")
except Exception as e:
print(f"Error browsing {url}: {e}")
self._random_delay(2, 6)
def warm_and_scrape(self, target_url, warmup_paths):
"""
Full warming sequence before scraping target
"""
# Step 1: Homepage
if not self.visit_homepage():
print("Homepage visit failed")
return None
# Step 2: Browse category pages
self.browse_random_pages(warmup_paths, count=random.randint(2, 4))
# Step 3: Scrape target
print(f"Scraping target: {target_url}")
response = self.session.get(target_url)
return response
# Usage example for e-commerce site
warmer = SessionWarmer(
base_url="https://www.example-store.com",
proxy="http://user:pass@residential-proxy.com:8080"
)
warmup_paths = [
"/",
"/categories",
"/categories/electronics",
"/search?q=laptop",
"/deals",
]
response = warmer.warm_and_scrape(
target_url="https://www.example-store.com/product/12345",
warmup_paths=warmup_paths
)
if response and response.status_code == 200:
print(f"Success! Retrieved {len(response.text)} bytes")
Behavioral Signals to Include
Your scraper should generate these signals that PerimeterX expects from real users:
Mouse movements: Dispatch mousemove events at random positions.
Scroll events: Scroll pages incrementally rather than jumping.
Click patterns: Click elements before navigating (where applicable).
Resource loading: Load images, CSS, and JavaScript files.
Session duration: Spend at least a few seconds on each page.
Referrer headers: Include proper Referer headers for internal navigation.
Using Residential Proxies for PerimeterX Bypass
All five methods perform significantly better when combined with high-quality residential proxies. IP reputation is a core component of PerimeterX's trust scoring.
Proxy Selection Guidelines
Residential IPs: Addresses assigned by ISPs to home users. Highest trust scores, most expensive.
Mobile IPs: Carrier IPs shared among mobile users. High trust, good for sites with mobile apps.
ISP Proxies: Datacenter IPs registered to ISPs. Middle ground between cost and trust.
Datacenter IPs: Server farm addresses. Cheap but heavily flagged. Avoid for PerimeterX sites.
If you need reliable proxy infrastructure, Roundproxies.com offers residential, datacenter, ISP, and mobile proxy options with rotation capabilities specifically designed for anti-bot bypass scenarios.
Proxy Integration Example
from curl_cffi import requests
import random
class ProxyRotator:
"""
Rotate through proxy pool with health monitoring
"""
def __init__(self, proxies):
self.proxies = proxies
self.failed_proxies = set()
self.success_count = {}
def get_proxy(self):
"""Get healthy proxy from pool"""
available = [p for p in self.proxies if p not in self.failed_proxies]
if not available:
# Reset if all proxies failed
self.failed_proxies.clear()
available = self.proxies
# Prefer proxies with higher success rates
weighted = []
for proxy in available:
weight = self.success_count.get(proxy, 1)
weighted.extend([proxy] * weight)
return random.choice(weighted)
def report_success(self, proxy):
"""Track successful proxy usage"""
self.success_count[proxy] = self.success_count.get(proxy, 1) + 1
if proxy in self.failed_proxies:
self.failed_proxies.remove(proxy)
def report_failure(self, proxy):
"""Track failed proxy"""
self.success_count[proxy] = max(1, self.success_count.get(proxy, 1) - 1)
# Temporarily blacklist after multiple failures
if self.success_count[proxy] <= 0:
self.failed_proxies.add(proxy)
# Usage
proxies = [
"http://user:pass@residential1.example.com:8080",
"http://user:pass@residential2.example.com:8080",
"http://user:pass@residential3.example.com:8080",
]
rotator = ProxyRotator(proxies)
session = requests.Session(impersonate="chrome131")
url = "https://protected-site.com"
for i in range(10):
proxy = rotator.get_proxy()
try:
response = session.get(
url,
proxies={"http": proxy, "https": proxy},
timeout=30
)
if response.status_code == 200:
rotator.report_success(proxy)
print(f"Request {i}: Success via {proxy}")
else:
rotator.report_failure(proxy)
print(f"Request {i}: Failed with {response.status_code}")
except Exception as e:
rotator.report_failure(proxy)
print(f"Request {i}: Error - {e}")
Troubleshooting Common PerimeterX Blocks
HTTP 403 Forbidden
This means PerimeterX blocked your request before JavaScript challenges. Check your TLS fingerprint and IP reputation first.
Solutions:
- Switch to
curl_cffiwith browser impersonation - Use residential proxies instead of datacenter
- Verify your headers match real browser patterns
"Press & Hold" Challenge Appearing
You passed initial checks but failed behavioral analysis or JavaScript fingerprinting.
Solutions:
- Switch to Camoufox for authentic browser fingerprints
- Add session warming before target requests
- Implement mouse movement and scroll behavior
- Increase delays between requests
Blocks After Initial Success
PerimeterX adapts to your patterns over time. Sustained scraping needs session rotation.
Solutions:
- Rotate browser profiles between sessions
- Change proxy IPs regularly
- Vary request patterns and timing
- Clear cookies and create fresh sessions
Token Expiration
PerimeterX tokens (stored in _px3 cookies) expire after approximately 60 seconds.
Solutions:
- Refresh sessions before tokens expire
- Implement automatic session recreation on 403 errors
- Don't cache sessions for extended periods
Quick Reference: Method Comparison
| Method | Best For | Resource Usage | Success Rate |
|---|---|---|---|
| Camoufox | Maximum stealth | High | 95%+ |
| curl_cffi | High-volume scraping | Low | 80-90% |
| Undetected ChromeDriver | Existing Selenium code | High | 70-85% |
| Playwright Stealth | Async scraping | Medium | 75-85% |
| Session Warming | Any method combination | Varies | +10-15% improvement |
Final Thoughts
Bypassing PerimeterX requires a layered approach. No single technique works reliably against all implementations.
Start with Camoufox for maximum stealth on heavily protected sites. Use curl_cffi when you need speed and scale. Combine any method with residential proxies and session warming for best results.
Remember that PerimeterX constantly evolves its detection methods. What works today may need adjustment tomorrow. Keep your tools updated and monitor your success rates.
For demanding scraping projects, consider whether building and maintaining bypass infrastructure makes sense versus using managed solutions. The cat-and-mouse game with anti-bots requires ongoing attention.
Happy scraping—and remember to respect rate limits and terms of service.
FAQ
Can I completely bypass PerimeterX forever?
No. PerimeterX uses machine learning that adapts to new bypass techniques. You can achieve high success rates, but expect occasional blocks and the need for ongoing adjustments.
Which method should I try first?
For most cases, start with curl_cffi for its simplicity and low resource usage. If you're getting blocked, upgrade to Camoufox for full browser fingerprint spoofing.
Are free proxies sufficient?
No. Free proxies and public VPNs are heavily flagged in anti-bot databases. Use quality residential proxies for reliable bypass.
Is bypassing PerimeterX legal?
Accessing publicly available data is generally legal, but always check the website's terms of service. This guide is for educational purposes. Use these techniques responsibly.
How do I know if I'm detected?
Watch for 403 status codes, challenge pages, sudden CAPTCHAs mid-session, or dramatically reduced success rates. These indicate PerimeterX has flagged your traffic.