HasData
Back to all posts

Cloudflare Error 1020, Causes and Fixes (2026)

Cloudflare Error 1020 occurs when Cloudflare’s Web Application Firewall (WAF) blocks your IP address based on firewall rules triggered by suspicious request patterns and connection-level fingerprints. Reducing these blocks typically involves residential proxies, realistic headers, request throttling, and headless browser configurations. Simple IP rotation or User-Agent changes fail because Cloudflare analyzes multiple connection layers simultaneously.

This guide covers the real mechanics behind Error 1020 and the tested methods that reduce it at scale

What Is Cloudflare Error 1020?

Cloudflare Error 1020 “Access Denied” occurs when a website’s firewall rules block your IP address due to suspicious or unauthorized activity. This error typically occurs when you violate security policies, such as attempting to access restricted content or sending excessive requests from your device.

During the WAF check, Cloudflare picks one of four outcomes. It lets you through to the original server, shows a CAPTCHA, runs JavaScript to check the client, or blocks the request outright. That’s when you see Error 1020, a Cloudflare-specific cousin of the plain 403. Its neighbours answer differently. 1010 blocks on the browser signature, and 520 means the origin server broke rather than blocked you.

Request path from DNS lookup through the Cloudflare WAF check to the origin server and back as a response, with the WAF step highlighted

Here are the main reasons this happens:

  1. Too many requests. If your script sends dozens of requests per second, Cloudflare sees it as an attack (the generic form of this is 429). Unlike error 1015, which is temporary, error 1020 results from higher request rates and can last much longer depending on Cloudflare’s threat assessment.
  2. Public proxies or VPN. IPs from data centers and free proxy lists are often blacklisted, and VPN exits with them. Bots prefer them, so Cloudflare doesn’t.
  3. Non-human behavior. No JavaScript, weird headers, hidden links, or anything that looks “too automated” makes Cloudflare suspicious.
  4. Geo-blocking. Some sites just block entire countries.

Most online advice tells users to clear their browser cache and cookies, turn off VPN or proxy, disable extensions, restart their router, or switch networks. But if you’re a developer running a scraping script, that won’t help. You’ll need a different set of tools, and that’s what we’ll get into next.

How to Reduce Cloudflare Error 1020 (7 Techniques)

To reduce Cloudflare error 1020 blocks, here’s what you can try:

  1. Proxy Rotation. Residential proxies with automatic rotation prevent IP-based blocking.
  2. Header Normalization. A request whose header set matches a real browser’s, fetch metadata and cipher suites included, is harder to separate from one.
  3. TLS Fingerprint Matching. curl_cffi sends a browser’s handshake where a plain Python client sends its own.
  4. Request Throttling. Random delays between requests mimic human browsing patterns.
  5. Headless Browser Patches. Undetected ChromeDriver or Puppeteer stealth plugins patch automation-identifying properties such as navigator.webdriver.
  6. Automated Captcha Solving. A category of third-party services answers challenge pages, Turnstile included, on behalf of the client.
  7. Web Scraping APIs. Managed services handle the proxies and rendering, retries included.

Now let’s break each of these methods down a bit more.

Use Residential Proxies with Rotation

When scraping data, it’s always better to use proxies. They help protect your real IP address. Which kind to use, and how rotating proxies differ from static ones, is its own subject. Residential exits from a paid provider are the ones that hold up. Free lists are mostly dead addresses and occasionally worse than that.

To use proxies in Python, you can go with the requests library:

import requests


proxies = {
    "http": "http://username:password@123.123.123.123:8080",
    "https": "http://username:password@123.123.123.123:8080"
}


response = requests.get("https://httpbin.org/ip", proxies=proxies)

Besides just using proxies, it’s better to rotate them, basically, switch them up constantly. That helps to avoid getting blocked. One way is to pick random proxies from a list for each request:

import random


proxies = [
    "http://user1:pass1@111.111.111.111:8000",
    "http://user2:pass2@222.222.222.222:8000",
    "http://user3:pass3@333.333.333.333:8000"
]


proxy = random.choice(proxies)

Or you can use rotating proxies. In that case, your proxy provider handles the rotation for you.

Set Realistic User-Agent and Headers

Next, you need to set the request headers. Sure, the User-Agent is one of the key ones, but it’s not the only sign that a script is run by a bot. Other important headers include:

HeaderPurpose
User-AgentIdentifies the browser
AcceptTells the server what content types are supported
Accept-LanguageLanguage preferences (should match typical browser settings).
RefererIndicates where the request came from (bots often skip it).
ConnectionNormally keep-alive in browsers.
Sec-Fetch-SitePart of browser fetch metadata (e.g., none, same-origin, etc.).
Sec-Fetch-ModeTypically navigate, cors, etc.
Sec-Fetch-DestIndicates the destination type (document, script, etc.).
Sec-Fetch-UserPresent only in top-level navigation with user action (?1).

To add headers to a request, pass them like this:

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


response = requests.get("https://httpbin.org/headers", headers=headers)

If you’re looking for a list of the latest User Agents and want to learn about them, there’s a separate post just for that.

Now, if the headers are already right and the bot flags are hidden, but you’re still hitting a 1020 error, it might be because Cloudflare has already identified you before a single header was even sent. This happens at the connection level, through what is known as a TLS fingerprint. In that case, try using tls-client.

from tls_client import Session


session = Session(client_identifier="chrome_136")
resp = session.get("https://example.com")
print(resp.status_code, resp.text[:100])

When your browser or script sets up an HTTPS connection, it shares a set of encryption settings (cipher suites, extensions). Cloudflare uses this info to create a short fingerprint called JA3. If the first HTTP request follows right after TLS, the order of its headers is used to make another fingerprint, JA4.

Libraries, like requests, use the default OpenSSL stack or built-in TLS modules, where cipher suites and extension order are different from a browser’s. More advanced libraries, like tls-client (which we used earlier), wrap browser-style TLS or tweak OpenSSL settings so your request looks more like a real browser in terms of JA3.

Match the TLS Fingerprint (curl_cffi)

Before Cloudflare reads a single header, it has already fingerprinted the TLS handshake. Every HTTP client negotiates ciphers and extensions in its own recognizable order, hashed as JA3, and Python’s requests announces itself at that layer no matter what User-Agent string travels above it. On a fingerprinting endpoint, requests presents its own JA3 hash and no HTTP/2 fingerprint at all (it speaks HTTP/1.1), which is exactly the combination WAF rules key on.

The curl_cffi library replays a real browser’s handshake instead:

from curl_cffi import requests

response = requests.get(
    "https://tls.browserleaks.com/json",
    impersonate="chrome",
)
print(response.json()["ja3_hash"])

On the same endpoint, impersonate="chrome" presented a Chrome JA3 hash together with a Chrome HTTP/2 (Akamai) fingerprint, and it sent the impersonation profile’s own Chrome User-Agent without being asked. The library ships impersonation profiles for current browser versions, and the one line replaces the whole header-mimicry exercise at the transport layer. Headers still matter above it, so the two techniques stack rather than compete.

Add Random Delays Between Requests

Another small trick to make your script feel more human and reduce the chance of being blocked by Cloudflare 1020 is to add small delays between your requests. Even better, make them random:

import time


delay = random.uniform(1.5, 4.0)
time.sleep(delay)

That way your scraping looks less suspicious.

Use a Headless Browser (Selenium, Puppeteer)

If you’re still getting hit with Cloudflare 1020, try using a headless browser. Keep using proxies and User-Agents, but now run the actual requests through something like Selenium:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options


options = Options()
options.add_argument(f'--proxy-server=http://user1:pass1@111.111.111.111:8000')
options.add_argument(f'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36')


driver = webdriver.Chrome(options=options)


driver.get("https://httpbin.org/ip")


driver.quit()

You can also run the browser in headless mode, allowing it to operate in the background. If you prefer coding in NodeJS, Puppeteer or Playwright might be a better fit.

Adjust Headless Browser Fingerprints

Cloudflare is usually pretty good at spotting headless browsers. The thing is, standard WebDrivers give off some clear signals that make them easy to detect:

  1. navigator.webdriver = true. This JavaScript property is automatically set when a page is loaded through WebDriver. In a real browser, it’s either undefined or false.

Chrome under WebDriver showing the automation banner, with the console returning true for navigator.webdriver

  1. Missing chrome.runtime. Chrome extensions use window.chrome.runtime to talk to the browser. In headless mode (especially with plain WebDriver), this object might be missing or incomplete, which can cause errors when accessed.
  2. Weird window.outerWidth/outerHeight values. Headless browsers often set these equal to innerWidth/innerHeight, or default to something fixed like 800×600. On a real device, these values are usually larger and don’t match inner dimensions.
  3. No plugins. Real users almost always have at least one plugin. If navigator.plugins.length is 0, that’s a red flag.

These signals come from the headless browser itself. Stealth plugins and “undetectable” browser modes adjust those fingerprints (navigator.webdriver, plugins and MIME types, window.chrome) so an automated browser behaves more like a regular one.

If you’re using Python, try undetected-chromedriver or SeleniumBase (it supports UC-mode, which is built on top of undetected-chromedriver):

from seleniumbase import SB


with SB(uc=True, headless=False) as sb:
    url = "https://httpbin.org/headers"
    sb.uc_open_with_reconnect(url, 3)
    html = sb.get_page_source()

This setup often avoids triggering a 1020 error. If you’re using Node.js, check out puppeteer-extra-plugin-stealth. It not only patches JavaScript fingerprints but also tweaks TLS settings to better mimic a real browser.

Cloudflare Turnstile Challenges

Turnstile is Cloudflare’s CAPTCHA replacement, and it’s what the challenge in front of a 1020-family block usually is now. It runs as a widget that collects browser signals (JavaScript execution and environment checks) and issues a token the protected site verifies. Managed mode shows a checkbox when the signals are unclear, non-interactive shows a spinner with no user action, and invisible renders nothing at all. A plain HTTP client never receives a token, so a Turnstile-guarded form or endpoint stays closed to it regardless of headers and proxies.

The widget’s cf-turnstile markup and a sitekey in the page source mark a challenge that only clears inside a real browser context. Waiting the way you would for a rate limit changes nothing. Headless browsers with stealth patches sometimes pass the non-interactive mode on clean residential exits, and Turnstile appears in the challenge types the solver services list.

Integrate Automated Captcha Solvers

Stealth has a ceiling. The WAF may force a challenge on every request regardless of the fingerprint, and a patched browser has nothing left to offer it. A market of solver services grew around that case, and each one carries its own terms and costs.

These services work through an API. The client sends the sitekey and the page URL taken from the target site, and the answer comes back as a token that goes into the page’s DOM or the request payload. The round trip adds latency to the pipeline, and the service sits outside HasData, with its own billing and its own rules about what it will answer.

Use a Web Scraping API

Using these browser setups, while effective, can be resource-intensive. Add proxy rotation and CAPTCHA-solving services, plus the fact that Cloudflare doesn’t always block instantly but runs multiple checks first, and the costs pile up quickly.

That’s why, in many cases, using a scraping API or a dedicated service might actually be a more practical solution. These tools keep the pipeline working as target sites change. Plus, you don’t have to worry about proxies, headless browsers, or maintaining the whole stack yourself.

One example is HasData’s web scraping API, which takes care of the entire scraping pipeline, from proxy management to rendering, with 99.9% uptime and fast response times. All you need is a Hasdata API key (you get it after signing up). Then set your parameters and get the content you need:

import requests
import json


api_key = "YOUR-API-KEY"


url = "https://api.hasdata.com/scrape/web"


payload = json.dumps({
  "url": "https://example.com",
  "proxyType": "datacenter",
  "proxyCountry": "US",
  "screenshot": True,
  "jsRendering": True
})
headers = {
  'Content-Type': 'application/json',
  'x-api-key': api_key
}


response = requests.request("POST", url, headers=headers, data=payload)

Check the docs for all the available options. And if there’s an API for the site you’re scraping (like Google SERP, Zillow, etc.), use it, it’ll save you time.

Full Code Example with Headless Browser, Custom Headers, and Delays

Here’s a full example combining a headless browser, custom headers, proxies, and request delays:

from seleniumbase import SB
import random
import time


proxies = [
    "http://111.111.111.111:8000",
    "http://222.222.222.222:8000"
]


user_agents = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36"
]


proxy = random.choice(proxies)
ua = random.choice(user_agents)


with SB(uc=True, headless=False, proxy=proxy, user_agent=ua) as sb:
    url = "https://httpbin.org/headers"
    sb.uc_open_with_reconnect(url, 3)

    time.sleep(random.uniform(2.5, 4.5))


    html = sb.get_page_source()

It also picks random proxies and User-Agents. You can change the script however you like.

Cloudflare 1020 Test Results and Metrics

To see if some of these methods actually work, we tested them on three websites with different levels of Cloudflare protection:

  1. Site A. Basic Cloudflare protection that checks for bots and sometimes throws a captcha.
  2. Site B. Active blocking is on for suspicious requests and repeated connections.
  3. Site C. Strict protection with advanced bot detection.

You can set up your own site and configure Cloudflare however you want, or build your own list and try the tests yourself. When you visit a Cloudflare-protected site, check the response headers, and you’ll find a cf-ray parameter, which is the Cloudflare Ray ID.

DevTools Network panel on a Cloudflare-protected site, with the Cf-Ray response header highlighted

To minimize randomness, each method was tested with 1,000 consecutive requests to each site. All tests were conducted on the same PC under identical conditions.

A request was marked as “successful” if it returned an HTTP status code 200 and the response contained the expected element on the page. Any other status code (e.g., 403, 1020) or a response requiring CAPTCHA was marked as “failure.”

Here’s what we got after trying different methods, both separately and combined:

Success rate per site:

TechniqueSite A (%)Site B (%)Site C (%)
No Proxy, No Headless634241
Datacenter Proxy Only553043
Residential Proxy + Headers716864
SeleniumBase + UC mode918977
API-Based (HasData API)999997

What each one cost to run:

TechniqueAvg. response (s)Avg. CPU (%)Avg. memory (%)
No Proxy, No Headless0.4413.2554.4
Datacenter Proxy Only2.3215.1156.1
Residential Proxy + Headers1.3611.353.6
SeleniumBase + UC mode5.0978.1676.1
API-Based (HasData API)4.3814.456.2

Without running scripts, CPU usage stays around 7%, and memory around 37%.

In general, these tests give you a rough idea of what works better against Cloudflare and what doesn’t. In production scraping, proxies are rotated or replaced as soon as they’re blocked, unlike in these tests.

Also, a lot depends on how strict the site’s Cloudflare settings are. Some pages hit you with a challenge or block (error 1020) immediately. Others let you through a few times before getting serious.

Which Method Should You Choose

The numbers above point at the trade-off. A browser-based setup reached 91, 89 and 77 percent and spent 78% of a CPU core doing it, which is affordable on a few hundred pages and not on a few hundred thousand. The API reached 99, 99 and 97 at 14% CPU, because the browser runs somewhere else.

Cloudflare’s rules change on their own schedule, so whichever route you take, the maintenance is recurring rather than one-off. Budget for it in either case.

Valentina Skakun
Valentina Skakun
Valentina is a software engineer who builds data extraction tools before writing about them. With a strong background in Python, she also leverages her experience in JavaScript, PHP, R, and Ruby to reverse-engineer complex web architectures.If data renders in a browser, she will find a way to script its extraction.
Articles

Might Be Interesting