How to Avoid Getting Your Proxy Blocked
Avoid proxy blocks with TLS fingerprint matching, realistic headers, and crawl rate control. Proxy detection bypass for Python and browser automation.

Most people trying to avoid proxy blocks focus entirely on the IP, rotating more proxies, switching providers, trying residential instead of datacenter. That fixes maybe 30% of blocks. The other 70% come from fingerprinting signals that have nothing to do with the IP address. Cloudflare, Akamai, DataDome, and PerimeterX all fingerprint your TLS handshake, HTTP headers, and browser behavior before they even look at the IP. This guide covers every layer of proxy detection bypass, from TLS fingerprinting to cookie handling, with code for Python and browser automation.
Why Proxies Get Blocked: The Full Picture
Sites use multiple independent detection layers. A proxy can fail any one of them regardless of how clean the IP is:
| Detection Layer | What Is Checked | How to Pass |
|---|---|---|
| IP reputation | IP in blocklists, ASN flagged as datacenter | Use clean datacenter IPs; rotate frequently |
| TLS fingerprint (JA3/JA4) | TLS client hello parameters, cipher suites, extensions, curves | Match browser TLS fingerprint with curl_cffi or tls-client |
| HTTP headers | User-Agent, Accept, Accept-Language, Accept-Encoding consistency | Send a full, internally consistent browser header set |
| HTTP version | HTTP/1.1 vs HTTP/2 vs HTTP/3 | Use HTTP/2 (httpx) to match modern browsers |
| Request rate | Requests per second per IP | Add delays; rotate proxies per request or per domain |
| Cookie/session consistency | Same session ID, different IP = suspicious | Bind one proxy to one session; reset cookies with proxy |
| JavaScript / browser behavior | navigator.webdriver, canvas fingerprint, mouse events | Use undetected-chromedriver or Playwright with stealth patch |
| Proxy headers (X-Forwarded-For) | Real IP leaked in proxy headers | Use proxies that strip these headers |
Changing the proxy only fixes the first row. The others require changes to your HTTP client or browser configuration.
Rotate Proxies to Avoid Rate Limits
Rate-based blocks are the most common and the easiest to fix. A target site counts requests per IP per time window, typically 10, 100 requests per minute before triggering a CAPTCHA or 429.
import time
import random
import requests
PROXIES = [
"http://your-proxy-1.sparkproxy.io:10000",
"http://your-proxy-2.sparkproxy.io:10001",
"http://your-proxy-3.sparkproxy.io:10002",
]
def get(url: str, min_delay: float = 1.0, max_delay: float = 3.0) -> requests.Response:
proxy = random.choice(PROXIES)
resp = requests.get(
url,
proxies={"http": proxy, "https": proxy},
timeout=15,
)
time.sleep(random.uniform(min_delay, max_delay)) # Human-like pacing
return resp
Two rotation strategies and when to use each:
| Strategy | When to Use | Implementation |
|---|---|---|
| Per-request rotation | Stateless pages, product listings, search results | `random.choice(PROXIES)` before each request |
| Per-session rotation | Login flows, multi-step checkouts, account-based scraping | One `requests.Session` per proxy; do not rotate mid-session |
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Fix Your TLS Fingerprint (JA3/JA4)
This is the most overlooked aspect of proxy detection bypass. Every TLS client sends a "Client Hello" message at the start of an HTTPS connection. The combination of cipher suites, TLS extensions, elliptic curves, and signature algorithms in this message creates a fingerprint, JA3 (older) or JA4 (newer). This fingerprint is computed entirely from the network packet, not from any header your code sets.
Python's requests library uses urllib3 under the hood, which has its own distinctive TLS fingerprint that matches no browser. Even if you set User-Agent: Mozilla/5.0 (Chrome/124), the TLS fingerprint still says "Python/urllib3." Cloudflare and DataDome block on this mismatch.
The fix: curl_cffi, a Python binding to curl that impersonates the actual TLS fingerprint of Chrome, Firefox, or Safari:
pip install curl_cffi
from curl_cffi import requests as cffi_requests
# Impersonates Chrome 124's exact TLS fingerprint, HTTP/2, and headers
resp = cffi_requests.get(
"https://httpbin.org/ip",
proxies={"http": "http://your-proxy.sparkproxy.io:10000",
"https": "http://your-proxy.sparkproxy.io:10000"},
impersonate="chrome124",
)
print(resp.json())
Available impersonation targets (as of curl_cffi 0.7):
| Impersonation Target | TLS + HTTP Version | Use When |
|---|---|---|
| `chrome124` | TLS 1.3, HTTP/2 | General-purpose, matches most desktop Chrome users |
| `chrome110` | TLS 1.3, HTTP/2 | Sites that specifically detect Chrome 124+ as too new |
| `firefox117` | TLS 1.3, HTTP/2 | Firefox user demographic |
| `safari17_0` | TLS 1.3, HTTP/2 | iOS/Mac targets with Safari-heavy traffic |
| `edge99` | TLS 1.3, HTTP/2 | Corporate or Microsoft-heavy environments |
curl_cffi also handles HTTP/2 automatically, requests only does HTTP/1.1 by default, which is another detectable mismatch against modern browsers.
Session-based usage with curl_cffi
from curl_cffi import requests as cffi_requests
session = cffi_requests.Session(impersonate="chrome124")
session.proxies = {
"http": "http://your-proxy.sparkproxy.io:10000",
"https": "http://your-proxy.sparkproxy.io:10000",
}
# All requests in this session use Chrome's TLS fingerprint
resp = session.get("https://example.com/login")
Set Consistent HTTP Headers
Even when your TLS fingerprint is correct, inconsistent HTTP headers reveal automation. A real Chrome 124 browser always sends a specific set of headers in a specific order. Sending User-Agent: Chrome/124 while omitting sec-ch-ua or sending Accept: / (Python default) instead of Chrome's real Accept header is a detectable anomaly.
Minimum consistent header set for Chrome 124 on Windows:
import requests
CHROME_HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"sec-ch-ua": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
}
session = requests.Session()
session.headers.update(CHROME_HEADERS)
session.proxies = {"http": "http://your-proxy.sparkproxy.io:10000",
"https": "http://your-proxy.sparkproxy.io:10000"}
Header order matters. Browsers send headers in a consistent, fixed order. HTTP/2 makes this especially detectable, the HPACK compression encodes header order into the frame.
curl_cffihandles this automatically. With plainrequests, you can userequests.structures.CaseInsensitiveDictonly for storage; the actual wire order may vary by Python version.
Combining curl_cffi for TLS + correct headers is the most reliable approach:
from curl_cffi import requests as cffi_requests
resp = cffi_requests.get(
"https://target-site.com",
headers={
"Accept-Language": "en-US,en;q=0.9",
"Referer": "https://www.google.com/",
},
impersonate="chrome124",
proxies={"http": "http://your-proxy.sparkproxy.io:10000",
"https": "http://your-proxy.sparkproxy.io:10000"},
)
curl_cffi already sets the correct TLS fingerprint, HTTP/2, and base browser headers for the chosen impersonation target. You only need to add request-specific headers like Referer.
Respect Crawl Rates and robots.txt
Sending 10 requests per second from the same proxy will trigger rate limiting before any fingerprint detection kicks in. Respecting the target site's intended crawl rate is both effective and ethical.
import time
import random
def polite_delay(min_s: float = 1.0, max_s: float = 4.0) -> None:
"""Sleep for a random human-like interval between requests."""
time.sleep(random.uniform(min_s, max_s))
Guidelines:
| Crawl Rate | Risk Level | When Appropriate |
|---|---|---|
| > 5 req/s per IP | High, near-certain block | Never |
| 1, 5 req/s per IP | Medium | Only with very large proxy pool |
| 1 req / 1, 3 s | Low | General scraping |
| 1 req / 5, 10 s | Very low | Sensitive sites, e-commerce, login-required |
To check a site's declared crawl rate:
import urllib.robotparser
rp = urllib.robotparser.RobotFileParser()
rp.set_url("https://example.com/robots.txt")
rp.read()
print(rp.can_fetch("*", "/products")) # True or False
print(rp.crawl_delay("*")) # Returns float or None
If crawl_delay returns a value, respect it. Ignoring Crawl-delay is one of the most common reasons scraper IPs get added to permanent blocklists.
Avoid Common Bot Detection Signals
Headless browser detection (Selenium / Playwright)
Selenium sets navigator.webdriver = true in the browser's JavaScript environment. This is readable by any JavaScript on the page and is the primary detection signal used by Cloudflare and DataDome for browser-based scraping.
Selenium fix, undetected-chromedriver:
pip install undetected-chromedriver
import undetected_chromedriver as uc
options = uc.ChromeOptions()
options.add_argument("--proxy-server=your-proxy.sparkproxy.io:10000")
driver = uc.Chrome(options=options)
driver.get("https://example.com")
undetected-chromedriver patches the Chrome binary to remove webdriver flags and other Selenium-specific modifications.
Playwright fix, stealth plugin:
pip install playwright playwright-stealth
playwright install chromium
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync
with sync_playwright() as p:
browser = p.chromium.launch(proxy={
"server": "http://your-proxy.sparkproxy.io:10000",
})
page = browser.new_page()
stealth_sync(page) # Patches navigator.webdriver and other leaks
page.goto("https://example.com")
browser.close()
Other bot signals to eliminate
| Signal | What Bots Do | What Real Browsers Do |
|---|---|---|
| Mouse movement | None, direct element click | Random Bezier-curve paths before clicking |
| Viewport size | 800ร600 default headless | 1280ร800 or 1920ร1080 common sizes |
| Timezone | UTC (server default) | Match the proxy's country timezone |
| WebGL renderer | SwiftShader / LLVMpipe (headless default) | Real GPU renderer string |
| Canvas fingerprint | Empty or identical across sessions | Unique noise per session |
For Playwright, set a realistic viewport and timezone:
context = browser.new_context(
viewport={"width": 1280, "height": 800},
locale="en-US",
timezone_id="America/New_York", # Match your proxy's datacenter location
proxy={"server": "http://your-proxy.sparkproxy.io:10000"},
)
Check for Proxy Header Leaks
Some proxies add X-Forwarded-For or X-Real-IP headers to outgoing requests, inadvertently revealing your real IP address to the target server. Always verify your proxy does not leak before using it in production.
import requests
def check_header_leak(proxy_url: str) -> dict:
proxies = {"http": proxy_url, "https": proxy_url}
resp = requests.get("https://httpbin.org/headers", proxies=proxies, timeout=10)
headers = resp.json().get("headers", {})
leak_headers = {
k: v for k, v in headers.items()
if k.lower() in ("x-forwarded-for", "x-real-ip", "via", "forwarded")
}
return {
"proxy": proxy_url,
"leak_headers": leak_headers,
"has_leak": bool(leak_headers),
}
result = check_header_leak("http://your-proxy.sparkproxy.io:10000")
print(result)
# {"proxy": "...", "leak_headers": {}, "has_leak": False} โ clean proxy
# {"proxy": "...", "leak_headers": {"X-Forwarded-For": "203.0.113.1"}, "has_leak": True} โ leaks real IP
If has_leak is True, switch to a proxy that strips these headers. SparkProxy datacenter proxies do not forward client IP headers.
Test Whether You Are Being Detected
Before deploying, test your setup against detection services:
from curl_cffi import requests as cffi_requests
def detection_test(proxy_url: str) -> None:
proxies = {"http": proxy_url, "https": proxy_url}
session = cffi_requests.Session(impersonate="chrome124")
session.proxies = proxies
tests = {
"Exit IP": "https://httpbin.org/ip",
"Headers": "https://httpbin.org/headers",
"Cloudflare check": "https://www.cloudflare.com/cdn-cgi/trace",
}
for name, url in tests.items():
try:
resp = session.get(url, timeout=10)
if name == "Cloudflare check":
# Parse key=value text response
data = dict(line.split("=", 1) for line in resp.text.strip().splitlines() if "=" in line)
print(f"[Cloudflare] ip={data.get('ip')} uag={data.get('uag', '')[:40]}")
else:
print(f"[{name}] {resp.status_code}: {resp.text[:120]}")
except Exception as exc:
print(f"[{name}] FAILED: {exc}")
detection_test("http://your-proxy.sparkproxy.io:10000")
https://www.cloudflare.com/cdn-cgi/trace returns the IP Cloudflare sees, the User-Agent string it received, and whether it thinks the request is from a bot. If uag matches your User-Agent header and ip matches the proxy IP, the request looks legitimate to Cloudflare.
Common Blocking Patterns and How to Respond
| Block Pattern | How to Identify | Fix |
|---|---|---|
| Immediate 403 on every request | IP in blocklist; or TLS fingerprint flagged instantly | Switch proxy; switch to curl_cffi with browser impersonation |
| 403 after N requests | Rate limit hit | Slow down; rotate proxy per request; add delays |
| CAPTCHA (JavaScript challenge) | Cloudflare "Checking your browser" page | Use curl_cffi; or switch to headless browser with stealth |
| Soft block: returns empty results | Anti-scraping at application layer (not HTTP) | Check session/cookie handling; simulate homepage visit first |
| 407 Proxy Authentication Required | Proxy credentials wrong or expired | Verify credentials in SparkProxy dashboard |
| Block only on HTTPS | CONNECT tunneling disabled; or TLS fingerprint mismatch | Switch to proxy with CONNECT support; use curl_cffi |
| Works from laptop, blocked from cloud | Cloud provider ASN flagged (AWS, GCP, Azure ranges known) | Use residential or mobile proxies instead of datacenter |
Frequently asked questions
Not automatically. Residential proxies have lower IP reputation risk, but TLS fingerprinting, header anomalies, and bot behavior signals still apply. A residential proxy combined with Python requests and a Python TLS fingerprint will still be blocked by Cloudflare. Fix the client fingerprint first; then choose the right proxy type.
JA3 is an older TLS fingerprinting method that hashes cipher suites, extensions, and elliptic curves from the TLS Client Hello. JA4 is a newer format (2023) that is more stable across TLS 1.3 sessions and adds transport-layer metadata. Both are computed at the network level, they cannot be spoofed by changing HTTP headers. Only curl_cffi (or a full browser) can generate a genuinely browser-matching JA3/JA4.
No, by itself. User-Agent is the most easily faked signal, so detection systems do not rely on it alone. They cross-check it against TLS fingerprint, HTTP/2 settings, sec-ch-ua headers, and request behavior. A mismatched set (Chrome User-Agent + Python TLS fingerprint) is more detectable than a consistent Python User-Agent + Python TLS fingerprint.
Use curl_cffi with impersonate="chrome124" for Python requests. For browser automation, use Playwright with playwright-stealth or undetected-chromedriver for Selenium. Also: rotate proxies per session, not per request (Cloudflare tracks challenge results per IP), and honor the site's crawl rate.
The most common causes: (1) the IP was added to a blocklist after previous scraping volume, (2) the target site updated its bot detection rules, (3) your request rate crossed the threshold. Actions: rotate to a fresh proxy, add delays, check whether TLS fingerprinting is now active on the target site.
IP reputation blocks are usually temporary (24, 48 hours for rate-limit blocks) or permanent (manual ban). You cannot unblock a banned IP. The correct action is to rotate to a new proxy. Focus on the fingerprinting and rate-limit fixes in this article so the new IP does not get blocked the same way.
Get 50% off your first purchase
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Offer ends soon โ claim it before it's gone
Related articles

How to Scrape Airbnb Listings and Prices
Learn how to scrape Airbnb listings and prices: extract fields from Airbnb's embedded JSON, handle date-based pricing, map pagination, and anti-bot defenses.

How to Scrape GraphQL APIs
Learn to scrape GraphQL API data: find the /graphql endpoint, read the query and variables in DevTools, then replay your own queries with cursor pagination.

How to Bypass reCAPTCHA When Web Scraping
How to bypass reCAPTCHA when web scraping the ethical way: how v2 and v3 scoring work, how to raise your reCAPTCHA score, and solvers as a last resort.
