๐ŸŽ‰ Premium Proxies ยท 3-Day Free TrialClaim Now โ†’
Guides

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.

S SparkProxy 157 14 min read
Share
How to Avoid Getting Your Proxy Blocked

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 LayerWhat Is CheckedHow to Pass
IP reputationIP in blocklists, ASN flagged as datacenterUse clean datacenter IPs; rotate frequently
TLS fingerprint (JA3/JA4)TLS client hello parameters, cipher suites, extensions, curvesMatch browser TLS fingerprint with curl_cffi or tls-client
HTTP headersUser-Agent, Accept, Accept-Language, Accept-Encoding consistencySend a full, internally consistent browser header set
HTTP versionHTTP/1.1 vs HTTP/2 vs HTTP/3Use HTTP/2 (httpx) to match modern browsers
Request rateRequests per second per IPAdd delays; rotate proxies per request or per domain
Cookie/session consistencySame session ID, different IP = suspiciousBind one proxy to one session; reset cookies with proxy
JavaScript / browser behaviornavigator.webdriver, canvas fingerprint, mouse eventsUse undetected-chromedriver or Playwright with stealth patch
Proxy headers (X-Forwarded-For)Real IP leaked in proxy headersUse 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:

StrategyWhen to UseImplementation
Per-request rotationStateless pages, product listings, search results`random.choice(PROXIES)` before each request
Per-session rotationLogin flows, multi-step checkouts, account-based scrapingOne `requests.Session` per proxy; do not rotate mid-session

proxy rotation


Free trial

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 TargetTLS + HTTP VersionUse When
`chrome124`TLS 1.3, HTTP/2General-purpose, matches most desktop Chrome users
`chrome110`TLS 1.3, HTTP/2Sites that specifically detect Chrome 124+ as too new
`firefox117`TLS 1.3, HTTP/2Firefox user demographic
`safari17_0`TLS 1.3, HTTP/2iOS/Mac targets with Safari-heavy traffic
`edge99`TLS 1.3, HTTP/2Corporate 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_cffi handles this automatically. With plain requests, you can use requests.structures.CaseInsensitiveDict only 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 RateRisk LevelWhen Appropriate
> 5 req/s per IPHigh, near-certain blockNever
1, 5 req/s per IPMediumOnly with very large proxy pool
1 req / 1, 3 sLowGeneral scraping
1 req / 5, 10 sVery lowSensitive 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.


Handle Cookies and Sessions Correctly

Sites use cookies to track session continuity. Two behaviors trigger blocks:

1. Rotating the proxy but keeping the same cookies. The site sees the same session ID arriving from two different IPs, a signal consistent with proxying. Fix: reset cookies when rotating to a new proxy.

2. Making requests with no cookies at all. Real browsers accumulate cookies progressively, homepage sets a session cookie, subsequent pages send it back. Scripts that skip directly to a product page with no cookies look like bots.

import requests
import random

PROXIES = [
    "http://your-proxy-1.sparkproxy.io:10000",
    "http://your-proxy-2.sparkproxy.io:10001",
]

def new_session(proxy_url: str) -> requests.Session:
    """Fresh session (no cookies) bound to one proxy."""
    session = requests.Session()
    session.proxies = {"http": proxy_url, "https": proxy_url}
    return session

def scrape_product(product_url: str) -> str:
    proxy   = random.choice(PROXIES)
    session = new_session(proxy)

    # Step 1: Hit the homepage to receive initial cookies
    session.get("https://example.com/", timeout=10)

    # Step 2: Now request the target page, cookies are sent automatically
    resp = session.get(product_url, timeout=10)
    return resp.text

Each call to new_session() creates a session with empty cookies. The homepage visit seeds the session with whatever cookies the site sets on first visit, making subsequent requests look like normal browsing.


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

SignalWhat Bots DoWhat Real Browsers Do
Mouse movementNone, direct element clickRandom Bezier-curve paths before clicking
Viewport size800ร—600 default headless1280ร—800 or 1920ร—1080 common sizes
TimezoneUTC (server default)Match the proxy's country timezone
WebGL rendererSwiftShader / LLVMpipe (headless default)Real GPU renderer string
Canvas fingerprintEmpty or identical across sessionsUnique 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 PatternHow to IdentifyFix
Immediate 403 on every requestIP in blocklist; or TLS fingerprint flagged instantlySwitch proxy; switch to curl_cffi with browser impersonation
403 after N requestsRate limit hitSlow down; rotate proxy per request; add delays
CAPTCHA (JavaScript challenge)Cloudflare "Checking your browser" pageUse curl_cffi; or switch to headless browser with stealth
Soft block: returns empty resultsAnti-scraping at application layer (not HTTP)Check session/cookie handling; simulate homepage visit first
407 Proxy Authentication RequiredProxy credentials wrong or expiredVerify credentials in SparkProxy dashboard
Block only on HTTPSCONNECT tunneling disabled; or TLS fingerprint mismatchSwitch to proxy with CONNECT support; use curl_cffi
Works from laptop, blocked from cloudCloud 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.


Limited-time ยท 50% off

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

Claim Discount

About the Author

SparkProxy Technical Team, The SparkProxy engineering team builds and maintains global datacenter and residential proxy infrastructure. This guide reflects anti-detection patterns validated with Python 3.11+, curl_cffi 0.7+, playwright-stealth 0.1.3+, and undetected-chromedriver 3.5+ (May 2026).

Citations: curl_cffi, Browser TLS impersonation for Python ยท Playwright Stealth, playwright-stealth on PyPI

Keep reading

Related articles

How to Scrape Airbnb Listings and Prices

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.

SparkProxyยทGuides
How to Scrape GraphQL APIs

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.

SparkProxyยทGuides
How to Bypass reCAPTCHA When Web Scraping

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.

SparkProxyยทGuides