Cloudflare Error 1020 Access Denied: Complete Fix Guide

Cloudflare Error 1020 Access Denied: Complete Fix Guide

If you’ve spent any time scraping the web, you’ve almost certainly encountered the dreaded “Error 1020 – Access Denied” page from Cloudflare. This error means Cloudflare’s firewall rules have identified your request as suspicious and blocked it outright.

Unlike Cloudflare’s JavaScript challenges (which give you a chance to prove you’re human), Error 1020 is a hard block. Your request is rejected with no opportunity to solve a challenge. This guide explains exactly why it happens and provides eight proven methods to fix it.

What Causes Cloudflare Error 1020?

Error 1020 is triggered by Cloudflare’s Web Application Firewall (WAF) rules. The site owner has configured specific rules that your request violates. Common triggers include:

1. Blocked IP Addresses or Ranges

The site owner has explicitly blocked your IP address or the entire IP range (common with datacenter IPs). Cloudflare’s firewall allows blocking by:

  • Individual IPs
  • CIDR ranges (e.g., entire /16 blocks from AWS or Google Cloud)
  • ASN (Autonomous System Number) — blocking all IPs from a specific provider
  • Country codes

2. Missing or Suspicious Headers

Firewall rules can inspect HTTP headers. Requests missing standard browser headers or containing automation-related headers get blocked:

# Headers that trigger 1020
User-Agent: python-requests/2.28.0
User-Agent: Go-http-client/1.1
User-Agent: (empty)

3. Blocked HTTP Methods

Some sites block non-standard HTTP methods. If you’re sending OPTIONS, PUT, or DELETE requests to endpoints that expect only GET or POST, the WAF may trigger 1020.

4. URL Pattern Matching

Firewall rules can match URL patterns. Accessing paths like /api/, /admin/, or /wp-json/ with non-whitelisted IPs often triggers 1020.

5. Rate-Based Rules

Cloudflare’s rate limiting can escalate to a 1020 block if you exceed defined thresholds. Unlike Error 1015 (which is specifically rate-limit), 1020 can be the result of a custom rule that monitors request rates.

6. Bot Score Threshold

Cloudflare assigns each request a “bot score” from 1-99. Site owners can configure WAF rules to block requests below a certain score. Automated tools typically receive very low scores.

Diagnosing the Exact Cause

Before jumping to solutions, identify which firewall rule is blocking you. The error page sometimes includes a Ray ID — a unique identifier for the blocked request.

import requests

try:
    response = requests.get("https://target-site.com")
    print(f"Status: {response.status_code}")
    print(f"CF-Ray: {response.headers.get('cf-ray', 'N/A')}")
except Exception as e:
    print(f"Error: {e}")

Check the response body for clues:

if response.status_code == 403:
    if "error code: 1020" in response.text:
        print("Cloudflare WAF rule triggered")
        # Extract Ray ID for debugging
        import re
        ray_match = re.search(r'Ray ID: ([a-f0-9]+)', response.text)
        if ray_match:
            print(f"Ray ID: {ray_match.group(1)}")

Fix 1: Switch to Residential Proxies

The most common cause of Error 1020 is using datacenter IPs. Switching to residential proxies immediately resolves this in most cases.

import requests

# Datacenter proxy (likely to trigger 1020)
dc_proxy = "http://user:pass@dc-proxy.provider.com:8080"

# Residential proxy (much less likely to trigger 1020)
res_proxy = "http://user:pass@res-proxy.provider.com:7777"

proxies = {
    "http": res_proxy,
    "https": res_proxy
}

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
    "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"
}

response = requests.get(
    "https://target-site.com",
    headers=headers,
    proxies=proxies
)

print(response.status_code)

For recommendations on residential proxy providers, see our proxy provider reviews.

Fix 2: Rotate IPs with Every Request

If your current IP is already blocked, rotating to a fresh IP can bypass the block.

import requests
from itertools import cycle

proxy_list = [
    "http://user:pass@gate.provider.com:7777",
    "http://user:pass@gate.provider.com:7778",
    "http://user:pass@gate.provider.com:7779",
]

proxy_pool = cycle(proxy_list)

def fetch_with_rotation(url, max_retries=5):
    for attempt in range(max_retries):
        proxy = next(proxy_pool)
        proxies = {"http": proxy, "https": proxy}

        try:
            response = requests.get(url, proxies=proxies, headers=headers, timeout=15)

            if response.status_code == 403 and "1020" in response.text:
                print(f"1020 on attempt {attempt + 1}, rotating IP...")
                continue

            return response
        except requests.exceptions.RequestException:
            continue

    return None

Read our detailed guide on IP rotation strategies for advanced rotation patterns.

Fix 3: Fix Your Request Headers

Incomplete or incorrect headers are a frequent cause. Here’s a comprehensive header set that mimics a real Chrome browser:

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.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,application/signed-exchange;v=b3;q=0.7",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br, zstd",
    "Connection": "keep-alive",
    "Upgrade-Insecure-Requests": "1",
    "Sec-Ch-Ua": '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
    "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",
    "Cache-Control": "max-age=0"
}

Header Order Matters

Some WAF rules check header order. Real browsers send headers in a specific order. Python’s requests library uses alphabetical ordering by default, which can be suspicious. Use OrderedDict or libraries that preserve insertion order.

from collections import OrderedDict
import requests

session = requests.Session()
session.headers = OrderedDict([
    ("Host", "target-site.com"),
    ("Connection", "keep-alive"),
    ("Cache-Control", "max-age=0"),
    ("Upgrade-Insecure-Requests", "1"),
    ("User-Agent", "Mozilla/5.0 ..."),
    ("Accept", "text/html,..."),
    ("Sec-Fetch-Site", "none"),
    ("Sec-Fetch-Mode", "navigate"),
    ("Sec-Fetch-User", "?1"),
    ("Sec-Fetch-Dest", "document"),
    ("Accept-Encoding", "gzip, deflate, br"),
    ("Accept-Language", "en-US,en;q=0.9"),
])

Fix 4: Use cURL-Impersonate for TLS Fingerprint Matching

Cloudflare’s WAF can detect mismatches between your claimed User-Agent and your actual TLS fingerprint. The curl_cffi library solves this.

from curl_cffi import requests as curl_requests

response = curl_requests.get(
    "https://target-site.com",
    impersonate="chrome",
    proxies={"https": "http://user:pass@proxy:port"}
)

if response.status_code == 200:
    print("Success! TLS fingerprint matched.")
else:
    print(f"Status: {response.status_code}")

For more on TLS fingerprinting, read our TLS/JA3 fingerprinting guide.

Fix 5: Browser Automation

When the WAF rule requires a real browser environment, use automation tools:

import undetected_chromedriver as uc

options = uc.ChromeOptions()
options.add_argument("--no-sandbox")

driver = uc.Chrome(options=options)
driver.get("https://target-site.com")

# Check if we bypassed the 1020
if "Access Denied" not in driver.page_source:
    print("Successfully bypassed 1020!")
    # Extract cookies for use with requests library
    cookies = {c['name']: c['value'] for c in driver.get_cookies()}
    print(f"Cookies: {cookies}")

driver.quit()

See our Undetected ChromeDriver tutorial for complete setup instructions.

Fix 6: Change Geographic Location

Some WAF rules block entire countries. If you’re scraping from a blocked region, switch to a proxy in an allowed country.

# Use country-specific proxy targeting
proxies = {
    "http": "http://user:pass@gate.provider.com:7777-country-us",
    "https": "http://user:pass@gate.provider.com:7777-country-us"
}

# Try different countries if US is blocked
countries = ["us", "gb", "de", "ca", "au"]
for country in countries:
    proxy = f"http://user:pass@gate.provider.com:7777-country-{country}"
    try:
        response = requests.get(
            "https://target-site.com",
            proxies={"http": proxy, "https": proxy},
            headers=headers,
            timeout=15
        )
        if response.status_code != 403:
            print(f"Success with country: {country}")
            break
    except:
        continue

Explore our geo-specific proxy guides for country-targeted scraping.

Fix 7: Use the Site’s API Instead

Many websites that heavily protect their frontend have less restrictive APIs. Check for:

  • REST API endpoints (look in Network tab of DevTools)
  • GraphQL endpoints
  • Mobile app APIs (often less protected)
# Instead of scraping the HTML page
# response = requests.get("https://site.com/products")  # Triggers 1020

# Use the API endpoint
api_response = requests.get(
    "https://api.site.com/v1/products",
    headers={
        "Authorization": "Bearer <token>",
        "Accept": "application/json",
        "User-Agent": "Mozilla/5.0 ..."
    },
    proxies=proxies
)

data = api_response.json()

Fix 8: Session-Based Approach

Cloudflare WAF rules often allow requests that come from established sessions. First visit a non-protected page to establish cookies, then navigate to the protected one.

session = requests.Session()
session.headers.update(headers)
session.proxies.update(proxies)

# Step 1: Visit homepage (usually less protected)
home_response = session.get("https://target-site.com/")
print(f"Homepage status: {home_response.status_code}")

# Step 2: Visit a simple page to accumulate cookies
about_response = session.get("https://target-site.com/about")
print(f"About page status: {about_response.status_code}")

# Step 3: Now try the protected page
target_response = session.get("https://target-site.com/protected-page")
print(f"Target status: {target_response.status_code}")

Automated Error Handling

Build resilience into your scraper with proper error handling:

import time
import random

class CloudflareHandler:
    def __init__(self, proxy_provider):
        self.session = requests.Session()
        self.proxy_provider = proxy_provider
        self.max_retries = 5

    def fetch(self, url):
        for attempt in range(self.max_retries):
            proxy = self.proxy_provider.get_proxy()
            self.session.proxies = {"http": proxy, "https": proxy}

            try:
                response = self.session.get(url, headers=headers, timeout=20)

                if response.status_code == 200:
                    return response

                if response.status_code == 403:
                    if "1020" in response.text:
                        print(f"Error 1020 on attempt {attempt + 1}")
                        # Rotate proxy and add delay
                        time.sleep(random.uniform(2, 5))
                        continue

                if response.status_code == 429:
                    wait = int(response.headers.get("Retry-After", 30))
                    print(f"Rate limited, waiting {wait}s")
                    time.sleep(wait)
                    continue

            except requests.exceptions.Timeout:
                continue

        return None

Prevention: Avoiding Error 1020 Proactively

Rather than fixing 1020 errors after they occur, prevent them:

  1. Start slow: Begin with 1-2 requests per minute and gradually increase
  2. Respect robots.txt: While not legally binding, sites that see robots.txt compliance are less likely to block you
  3. Rotate everything: IPs, User-Agents, and even request patterns
  4. Add realistic delays: Real users don’t send requests every 100ms
  5. Maintain sessions: Reuse cookies within a session window

For comprehensive scraping best practices, see our guides on rate limiting and user-agent rotation.

Conclusion

Cloudflare Error 1020 is a firewall-level block that requires a different approach than JavaScript challenges. Start with residential proxies and proper headers (Fixes 1-3), which resolve the majority of cases. Escalate to TLS fingerprint matching or browser automation only when needed.

The key insight is that 1020 errors are usually caused by who you are (your IP) or how you look (your headers and fingerprint), not what you do. Fix those two things, and most 1020 blocks disappear.

For broader Cloudflare bypass strategies, read our comprehensive Cloudflare bypass guide.


Related Reading

Scroll to Top