Cloudflare Error 1020 Access Denied: Playwright Fixes

What Cloudflare Is Actually Checking
If you’re hitting cloudflare error 1020 access denied or its cousins (1015, 1010, 1012, 1006, 1007) from Playwright or Puppeteer, you’re running into Cloudflare’s mitigation stack, not a random bug.
At a high level Cloudflare evaluates:
- IP reputation & ASN – is your IP in a datacenter range, shared by abuse, or previously flagged?
- Rate & concurrency – are you sending too many requests per IP / per cookie / per session?
- Browser fingerprint – headers, TLS, HTTP/2/3 settings, JS and DOM features, canvas/WebGL, timing.
- Behavioral signals – navigation patterns, mouse/scroll events, challenge solving, Turnstile completion.
- Account rules – per-zone firewall rules, WAF rulesets, country blocks.
Different errors map to different slices of that system. The fix that holds is always: match real traffic on all the dimensions Cloudflare cares about. Swapping User-Agent or adding a stealth plugin only touches a thin layer.
We’ll walk through:
- How to tell which error you’re really hitting (including
403/503variants). - How to decode Cloudflare headers (
cf-ray,cf-mitigated,server-timing). - Durable mitigation strategies for Playwright and Puppeteer.
- Why simple user-agent rotation and headless flags are no longer enough.
If you’re designing a more general-purpose agent/browser stack, the broader patterns also tie into the anti-bot cluster here: AI Agent Browser Automation vs “Just Use Playwright”.
Reading Cloudflare Headers From Node
Before guessing, inspect what Cloudflare is telling you. Errors are split between:
- Explicit Cloudflare pages (
/cdn-cgi/l/chk_jschl,error code: 1020in body). - Plain HTTP status codes with Cloudflare-specific headers.
Useful headers:
cf-ray: per-request ID; confirms Cloudflare actually handled the request.cf-mitigated: present when a mitigation rule fired (e.g.challenge,block,captcha).server-timing: often includescf-q-config;dur=?,desc="xyz"giving hints which rules fired.cf-cache-status: tells you if the response was a cache hit; usually irrelevant for errors.
From Playwright you can inspect errors like this:
// playwright-cloudflare-debug.js
import { chromium } from 'playwright';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
page.on('response', async (response) => {
const url = response.url();
const status = response.status();
const headers = response.headers();
if (status >= 400 || headers['cf-mitigated']) {
console.log('\n=== Cloudflare debug ===');
console.log('URL:', url);
console.log('Status:', status);
console.log('cf-ray:', headers['cf-ray']);
console.log('cf-mitigated:', headers['cf-mitigated']);
console.log('server-timing:', headers['server-timing']);
}
});
await page.goto('https://target.example/', { waitUntil: 'domcontentloaded' });
await browser.close();
})();In Puppeteer the pattern is similar:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
page.on('response', async (response) => {
const headers = response.headers();
if (headers['cf-ray']) {
console.log('CF response', response.status(), headers['cf-mitigated']);
}
});
await page.goto('https://target.example/');Collect real headers from your failing requests before changing your setup. Most of the confusion around 1020/1015/1010 is guessing without looking.
Error 1020: Access Denied (Firewall Rule / IP Reputation)
Error 1020: Access denied is Cloudflare’s way of saying “a firewall rule said no”. That rule may be explicit (zone owner block rule) or implicit (managed WAF / IP reputation).
Typical characteristics:
- HTML page with
Error 1020text from Cloudflare. - Status code: usually
403. cf-raypresent.- Often
cf-mitigated: blockor similar.
Common triggers for automation:
- IP in a blocked ASN (datacenter ranges, VPN providers).
- IP with bad reputation from previous traffic.
- Country blocks.
- Custom rules like “block if User-Agent contains
Puppeteer” or “block if no JS cookies”.
Durable fixes that hold:
- Residential / consumer IPs instead of datacenter ranges.
- Stable IP pools rather than burning through thousands of IPs.
- Realistic browser stack: real browser, full JS, cookies enabled.
Anti-patterns that rarely hold:
- Just changing
User-Agent. - Toggling headless off while keeping everything else default.
- Blindly rotating through cheap proxies with terrible reputation.
If you don’t want to build this stack yourself, tools like Human Browser — pay as you go, no subscription sit on top of residential IPs and real browser execution, exposing a simple agent API.
Playwright: Hardening Against 1020
Cloudflare tends to dislike default dev-tool fingerprints. Make your Playwright session closer to a real user:
import { chromium } from 'playwright';
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' +
'AppleWebKit/537.36 (KHTML, like Gecko) ' +
'Chrome/124.0.0.0 Safari/537.36',
javaScriptEnabled: true,
});
const page = await context.newPage();
// Do not block images/CSS/JS – Cloudflare uses them for fingerprinting
await page.goto('https://target.example/', {
waitUntil: 'networkidle',
timeout: 45_000,
});
console.log('Content length:', (await page.content()).length);
await browser.close();
})();Combine that with a residential proxy layer and you’ll see many 1020s disappear.
Error 1015: You Are Being Rate Limited

Error 1015 is explicitly about rate limiting.
Signals:
- Status usually
429or sometimes403. - HTML with
You are being rate limited/Error 1015. cf-mitigatedmay showlimit/block.
Cloudflare can rate-limit:
- Per IP
- Per cookie / session
- Per path or route
Bot-style patterns it dislikes:
- Hundreds of requests per second from one IP.
- Parallel crawls without pacing.
- Bursts of traffic after long idle periods.
Durable fixes:
- Global rate limiting per IP (e.g. 1–3 pages/sec, depending on site weight).
- Backoff on
429with exponential or at least linear wait. - Spreading load over multiple IPs without hammering any single one.
Example pacing logic with Playwright:
// crude token-bucket style pacing per context
class Pacer {
constructor({ intervalMs, maxTokens }) {
this.intervalMs = intervalMs;
this.maxTokens = maxTokens;
this.tokens = maxTokens;
setInterval(() => {
this.tokens = Math.min(this.maxTokens, this.tokens + 1);
}, this.intervalMs);
}
async consume() {
while (this.tokens <= 0) {
await new Promise(r => setTimeout(r, this.intervalMs));
}
this.tokens -= 1;
}
}
const pacer = new Pacer({ intervalMs: 1000, maxTokens: 2 });
async function safeGoto(page, url) {
await pacer.consume();
const response = await page.goto(url, { waitUntil: 'domcontentloaded' });
if (response?.status() === 429) {
console.warn('Hit 429, backing off');
await new Promise(r => setTimeout(r, 10_000));
}
}Distinguishing 1015 From 1020 Using Headers
| Symptom | 1015 rate limited | 1020 access denied |
|---|---|---|
| Page text | "You are being rate limited" | "Error 1020: Access denied" |
| Status code | 429 (usually) | 403 |
cf-mitigated header | often limit / challenge | often block / ban |
| Behavior after cooldown | Eventually recovers | Stays blocked until IP / rule changes |
| Primary root cause | Too many requests | Firewall rule / IP / country / reputation |
If errors vanish after slowing down, it was 1015. If they persist indefinitely, suspect 1020 or a ban code.
Error 1010: Browser Signature Mismatch
Error 1010 generally points at Cloudflare deciding “this is not a real browser”. You’ll often see it after trying to be clever with headers or headless flags.
Typical triggers with Playwright/Puppeteer:
- Incomplete header sets (missing
Sec-CH-UA*,Accept-Language, etc.). - Headless-only quirks: canvas fingerprint, WebGL vendor, missing fonts.
- Disabled JS or blocked resources.
Durable fixes:
- Use real browsers via Playwright/Puppeteer instead of HTTP-only clients.
- Stop trying to micro-tune every header. Let Chromium set most headers.
- Avoid breaking scripts with overzealous request interception.
For Puppeteer, avoid obviously-automated launch options like forcing very small viewports, unusual language combos, or stripping all plugins. Stick close to defaults and focus on IP/frequency.
Errors 1012, 1006, 1007: Banned or Blocked IP
These codes are variations on “this IP is not welcome here anymore”. They often show as Cloudflare stock pages with those codes in body text.
Common causes:
- Repeated abuse-like traffic from a specific IP.
- Tripping multiple WAF rules over time.
- Using exit nodes that are heavily abused globally.
Durable response:
- Rotate away from that IP/ASN completely; do not hammer it further.
- Reduce per-IP concurrency and aggression before switching.
- Use providers with clean residential ranges, not random public VPNs.
If you see 1006/1007/1012 plus cf-mitigated: ban (or no cf-mitigated, but the same IP fails across paths), that IP is effectively dead for that zone.
403 With cf-mitigated: Silent Blocks
Sometimes you won’t see a pretty Cloudflare error page. Instead you get a 403 with HTML from the origin, but headers show that Cloudflare interfered.
Signs:
- Status
403. cf-raypresent.cf-mitigatedset to something non-empty.
This usually means:
- The request matched a managed WAF rule.
- The site owner has custom rules (
/loginrules, bot score thresholds, etc.).
Your mitigation strategy is the same stack as 1020, but more subtle:
- Check
server-timingfor hints; sometimes descriptions map to rule sets. - Start by reducing concurrency and switching to a better IP pool.
- Test manually from a real browser on the same IP; if that fails too, the IP or country is simply blocked.
503 Challenge Loops and Turnstile

Another pattern: 503 plus a Cloudflare challenge that never resolves in automation, sometimes looping forever.
Characteristics:
- Status
503withcf-ray. - Body is a challenge page (JS challenge, Turnstile, or similar).
- Re-trying the same URL just replays the challenge.
Today most challenges are Turnstile-based: a widget rendered in the page that you must solve (often automatically, but associated with your IP/session and site key).
If you run headless Chromium and ignore the widget, you’ll loop forever.
Durable fixes:
- Let a real browser execute the challenge and store the solved cookies.
- Maintain session continuity (reuse cookies, same IP, same user-agent).
- Respect navigation flows instead of directly hitting final endpoints.
This is the main reason tooling like Human Browser exists: the service provides:
- Real browser execution (not just HTTP) under the hood.
- Residential proxy routing.
- An API surface you can hit from agents or scripts.
If you want to integrate a service like that into an agent workflow, the A2A endpoint is documented at https://agent.humanbrowser.cloud/a2a, and there’s a simple install flow at /install.
Playwright vs Puppeteer Against Cloudflare
Playwright and Puppeteer are both capable of behaving like real browsers when used correctly. The important bits are outside the test runner: IPs, pacing, and avoiding obvious fingerprints.
| Aspect | Playwright | Puppeteer |
|---|---|---|
| Browsers supported | Chromium, Firefox, WebKit | Chromium, Firefox (via DevTools/WebDriver BiDi) |
| Default headless | Headless by default for tests | Headless by default |
| Install experience | npm init playwright@latest sets up config | npm i puppeteer downloads compatible Chrome |
| Typical Cloudflare risk | Parallel tests hammering same IP / profile | Minimal header tweaks breaking fingerprint |
| Mitigation knobs | Contexts with UA/viewport/locale per project | Browser launch + per-page header control |
You can make either work; pick the one you’re comfortable hardening, and focus on the network layer rather than chasing one more stealth plugin.
Putting It Together: A Durable Mitigation Stack
Treat Cloudflare as an adversarial system. To make your automation survive over months instead of days:
- Decode errors correctly
- Log
status,cf-ray,cf-mitigated,server-timingper request. - Distinguish: rate (1015) vs firewall/ban (1020/1006/1007) vs fingerprint (1010) vs challenge (503/403 with challenge HTML).
- Stabilise IP & ASN
- Prefer residential IPs with good reputation.
- Keep per-IP concurrency low and predictable.
- Behave like a real user
- Use Playwright or Puppeteer with default browser settings wherever possible.
- Avoid stripping resources required for fingerprinting.
- Keep JS enabled; don’t fight the browser.
- Pace traffic
- Global per-IP QPS limits.
- Backoff on 429 / 503.
- Randomised intervals and human-ish bursts instead of robots-on-the-dot.
- Handle challenges
- For Turnstile/JS challenges, use a browser-based solver, not raw HTTP.
- Reuse solved sessions, don’t force a challenge on every page.
When you outgrow simple scripts, it’s often easier to step up to a managed anti-bot-aware browser layer than to re-implement all of this yourself. That’s the design space tools like Human Browser live in: real browsers, residential IPs, and an API that agents and scrapers can call when plain Playwright or Puppeteer keep slamming into cloudflare error 1020 access denied.
Where This Fits In Anti-Bot Architecture
All of this sits inside a broader anti-bot landscape:
- Cloudflare is one of several major managed WAF/anti-bot stacks.
- The same structural issues (IP reputation, fingerprinting, behavioral analysis) reappear across vendors.
If you’re designing a system that should outlive tweaks to a single provider, think in terms of:
- Capability layers: navigation, DOM interaction, challenge solving, proxy routing, logging.
- Pluggable backends: raw Playwright, Puppeteer, Human Browser, other remote browsers.
- Policy: how your agents decide when to slow down, rotate IPs, or escalate from simple HTTP to full browser sessions.
The anti-bot cluster article, AI Agent Browser Automation vs “Just Use Playwright”, goes deeper into how to design these layers so Cloudflare-specific fixes don’t infect your whole codebase.
Frequently asked questions
What does Cloudflare error 1020 access denied mean for Playwright?
Cloudflare error 1020 means a firewall rule blocked your request, often due to IP reputation, country blocks, or WAF rules. Fixes usually require better IPs and more realistic browser behavior, not just a new User-Agent string.
How do I fix Cloudflare 1015 rate limited in Puppeteer?
Implement global per-IP rate limiting, reduce parallel page loads, and back off when you see 429 responses. If requests resume working after a cooldown, your pacing is the issue, not fingerprinting.
Why does Cloudflare show error 1010 when I use headless Chrome?
Error 1010 is generally a browser signature mismatch. Headless defaults, stripped headers, or blocked scripts make your session look non-human. Use standard browser settings, keep JS enabled, and avoid over-customising headers.
How can I detect if Cloudflare is blocking me via headers?
Check for `cf-ray`, `cf-mitigated`, and `server-timing` headers on 4xx/5xx responses. A 403/429/503 with these headers usually indicates Cloudflare mitigation even if the page is branded like the origin site.
Can I bypass Cloudflare challenges with plain HTTP clients?
Not reliably. Modern Cloudflare challenges rely on full browser execution and Turnstile widgets. You typically need a real browser environment like Playwright, Puppeteer, or a managed browser service that can execute the page’s JavaScript and maintain cookies.
When should I move from raw Playwright to a managed browser service?
If you keep hitting 1020/1010/503 loops despite tuning headers, pacing, and proxies, or you need large-scale residential IP coverage and automatic challenge handling, a managed browser layer such as Human Browser can be more effective than extending Playwright scripts indefinitely.