Meta Description: API calls getting blocked by Cloudflare with 403 or 1020 errors? Here’s how I diagnosed it and fixed it — WAF rules, bot mode, headers, and more.
The first time this bit me, I’d just finished a working integration the night before, went to bed, and woke up to every request from my backend returning a raw HTML challenge page instead of JSON. Nothing in my code had changed. The target site had turned on a Cloudflare security feature overnight, and every one of my server-to-server API calls started getting blocked as if I were a bot — because, from Cloudflare’s point of view, I basically was.
This is one of the more disorienting problems to debug because the failure doesn’t come from your API provider’s actual application code — it comes from the CDN/security layer sitting in front of it. Once I understood how Cloudflare’s security stack is structured, though, the fix is almost always a matter of identifying which specific layer is doing the blocking and addressing that one thing, instead of throwing random headers and retries at the problem.
Here’s the full diagnostic and fix process I use now, including the code I run to confirm exactly what’s happening before I touch a single setting.
Why Cloudflare Blocks Legitimate API Traffic
Cloudflare sits in front of a huge share of the internet’s APIs as a reverse proxy, and it layers several independent security systems on top of the origin server:
- WAF (Web Application Firewall) — managed and custom rulesets that inspect request patterns and can flag things that look like SQL injection, scraping, or exploit attempts, even when your request is completely legitimate.
- Bot Fight Mode / Super Bot Fight Mode — heuristics designed to detect automated traffic. Server-to-server API calls look exactly like bot traffic to these heuristics, because technically, they are automated requests.
- Rate Limiting Rules — per-IP or per-path thresholds that block or challenge traffic exceeding a set number of requests in a time window.
- Security Level / Under Attack Mode — a site-wide dial that, when turned up, adds JavaScript challenges or CAPTCHAs to more traffic, including API endpoints if they’re not explicitly excluded.
- IP Reputation and ASN blocking — Cloudflare maintains reputation scores for IP ranges, and traffic from certain data center or cloud provider IP ranges (AWS, GCP, Azure, DigitalOcean) gets treated with more suspicion by default, since a lot of scraping and abuse also originates from those same ranges.
- TLS fingerprinting (JA3/JA4) — some Cloudflare configurations flag requests whose TLS handshake signature doesn’t match a real browser, which affects some HTTP client libraries more than others.
The important thing I had to internalize: none of this is Cloudflare being broken. It’s doing exactly what it was configured to do. The fix isn’t about tricking Cloudflare — it’s about correctly identifying your traffic as legitimate through the mechanisms Cloudflare actually provides for that.
Step 1: Identify Exactly Which Error You’re Getting
Before changing anything, I check the actual HTTP status code and any Cloudflare-specific error code, because each one points to a completely different subsystem.
curl -I https://api.example.com/v1/endpoint
Look at the status code and, if present, the cf-ray header (this is your Cloudflare request ID — save it, you’ll need it if you end up contacting the site owner or support).
| Code | Meaning | Layer Responsible |
|---|---|---|
| 403 Forbidden | Blocked by WAF rule or firewall rule | WAF / Firewall Rules |
| 503 (with Cloudflare error page) | Under Attack Mode or JS challenge required | Security Level |
| 1015 | Rate limited by Cloudflare | Rate Limiting Rules |
| 1020 | Blocked by a specific firewall/WAF rule (Access Denied) | WAF / Custom Rules |
| 1010 | Blocked due to browser signature/User-Agent | Bot Fight Mode |
| 1012 | Access denied, country-level block | Firewall Rules (geo) |
| 522 | Connection timed out to origin | Origin server, not Cloudflare security |
| 525 | SSL handshake failure with origin | TLS config between Cloudflare and origin |
In my case it was a 1020, which told me immediately this was a firewall rule and not a rate-limit or bot-heuristic issue — that narrowed the fix down before I’d touched any settings.
Step 2: Decide Which Side of the API You’re On
This matters a lot for which fixes are available to you:
- You control the origin server (it’s your own API behind your own Cloudflare account) — you have full access to WAF rules, firewall rules, and bot settings. Skip to Step 3.
- You’re consuming a third-party API that happens to sit behind Cloudflare — you can’t change their settings, so your options are limited to making your requests look as legitimate as possible (Step 6) and, if that’s not enough, contacting the provider to get allowlisted.
If it’s your own infrastructure, this is usually a quick fix. If it’s a third-party API, I’ve had to go through their support to get a static IP allowlisted more than once — annoying, but it’s a five-minute email, not a code problem.
Step 3: Check Firewall Events (If You Own the Cloudflare Account)
Cloudflare’s dashboard under Security > Events shows exactly which rule triggered the block, including the rule name and the specific request that got flagged. This is the single most useful screen for this problem — don’t guess, look here first.
If you’re scripting this, the Cloudflare API can pull the same data:
curl -X GET "https://api.cloudflare.com/client/v4/zones/{zone_id}/security/events" \
-H "Authorization: Bearer {api_token}" \
-H "Content-Type: application/json"
This tells you the rule ID responsible, which you can then either adjust or exclude your API path from.
Step 4: Create a Firewall Rule Exception for Your API Path
Once I know which rule is blocking legitimate traffic, the fix is usually a scoped exception rather than disabling the rule entirely (which would remove protection for the whole site).
In the dashboard: Security > WAF > Custom Rules, create a rule like:
- If: URI Path starts with
/api/AND (optionally) Known Bot isfalseAND IP Source Address is in{your known API client IPs} - Then: Skip → remaining custom rules, or specifically the managed rule causing the block
Doing it via the API (useful if you’re managing this as infrastructure-as-code):
const response = await fetch(
`https://api.cloudflare.com/client/v4/zones/${zoneId}/rulesets/${rulesetId}/rules`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
action: "skip",
action_parameters: {
ruleset: "current",
},
expression: '(http.request.uri.path contains "/api/") and (ip.src in {203.0.113.10 203.0.113.11})',
description: "Allow known API clients through WAF",
enabled: true,
}),
}
);
const data = await response.json();
console.log(data);
I scope this to specific known IPs where possible instead of exempting the whole /api/ path from all rules — an unscoped exception is a real security gap, and it’s one of the more common mistakes I see when people fix this too broadly.
Step 5: Adjust Bot Fight Mode for API Traffic
If Bot Fight Mode or Super Bot Fight Mode is the culprit (you’ll usually see this as a 1010 error, or requests silently getting a JS challenge page instead of JSON), the fix depends on your plan:
- On plans with Super Bot Fight Mode, you can configure “Definitely Automated” traffic to be allowed for specific paths instead of blocked, using a Custom Rule that checks
cf.bot_management.verified_botor scopes by path. - Consider issuing API tokens or mTLS client certificates to trusted automated clients instead of trying to make them pass as browser traffic — this is the more correct long-term solution than fighting the bot detection.
- For your own scripts hitting your own API, whitelisting by IP (Step 4) combined with disabling bot checks specifically on
/api/*paths is the cleanest approach.
Step 6: Fix Your Request Headers (When You Don’t Control the Origin)
If you’re consuming someone else’s API and can’t touch their Cloudflare settings, the most common fix is making sure your HTTP client isn’t tripping obvious bot signals unintentionally.
import requests
headers = {
"User-Agent": "MyApp/1.0 (contact@example.com)",
"Accept": "application/json",
}
response = requests.get("https://api.example.com/v1/endpoint", headers=headers, timeout=10)
print(response.status_code)
A few things I check specifically:
- Missing or default User-Agent — some HTTP libraries (older versions of
requests,curlwith no-Aflag, bareaxiosdefaults) send a generic string that’s an easy bot signal. Set an explicit, identifiable one. - Missing
Acceptheader — API requests without a properAccept: application/jsonheader sometimes get treated as browser navigation and served a challenge page instead of an API response. - TLS library quirks — some lightweight HTTP clients don’t negotiate TLS the same way a real browser does, which can trip JA3-based bot detection. Switching to a well-maintained client library (or making sure you’re on a current version) sometimes resolves this on its own.
None of this is about spoofing a browser to sneak past security — it’s about not accidentally looking like a scraper when you’re not one.
Step 7: Implement Retry Logic With Backoff for Rate Limiting
If the issue is a 1015 (rate limited) rather than an outright block, the fix is respecting the limit, not evading it.
async function fetchWithBackoff(url, options = {}, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429 && response.status !== 1015) {
return response;
}
const retryAfter = response.headers.get("Retry-After");
const delay = retryAfter
? parseInt(retryAfter, 10) * 1000
: Math.min(1000 * 2 ** attempt, 30000);
await new Promise((resolve) => setTimeout(resolve, delay));
}
throw new Error("Max retries exceeded due to rate limiting");
}
This respects the Retry-After header when the origin sends one, and falls back to exponential backoff when it doesn’t. In my experience this alone resolves a big chunk of “intermittent” Cloudflare blocking that isn’t actually a hard block — it’s a rate limit you’re hitting because of tight polling intervals.
Cloudflare Security Level Comparison
| Setting | Effect on API Traffic | When I’d Use It |
|---|---|---|
| Essentially Off | Minimal challenges | Internal APIs with IP allowlisting already in place |
| Low / Medium | Challenges only clearly suspicious traffic | Public APIs with light abuse history |
| High | Challenges most traffic without a strong reputation | Sites under active, ongoing abuse |
| Under Attack Mode | JS challenge for nearly everything, including most API clients | Active DDoS only — should never be left on permanently for an API-serving domain |
If you’re running your own API behind Cloudflare, leaving Under Attack Mode on by default is the single most common self-inflicted cause of “Cloudflare is blocking my API requests” reports from your own integration partners.
Common Mistakes I’ve Made (So You Don’t Have To)
- Disabling the WAF entirely instead of scoping an exception — fixes the symptom, removes real protection.
- Assuming it’s a code bug and spending an hour debugging application logic before checking the actual HTTP status code and Cloudflare error code first.
- Spoofing a full browser User-Agent on a server-to-server integration instead of getting properly allowlisted — this works until Cloudflare’s heuristics update, then breaks again with no clear error.
- Not saving the
cf-rayID when reporting the issue to a third-party API’s support team — it’s the fastest way for them to find the exact blocked request in their logs.
Frequently Asked Questions
Why does Cloudflare block my API requests but not my browser requests? API clients don’t execute JavaScript challenges or carry the same TLS/browser fingerprint as a real browser, so Cloudflare’s bot detection treats them as automated traffic by default — which, technically, they are. The fix is explicit allowlisting or bot-management exceptions, not disguising the traffic.
What does Cloudflare error 1020 mean? It means a specific firewall or WAF rule matched your request and explicitly denied access. Check Security > Events in the Cloudflare dashboard (if you own the zone) to see exactly which rule fired.
How do I whitelist my server’s IP in Cloudflare? Create a Custom Firewall Rule with a condition matching your IP address (or CIDR range) and set the action to “Allow” or “Skip” for the relevant security checks, scoped to your API path where possible.
Why am I getting a 503 error from Cloudflare on API calls? A 503 with a Cloudflare challenge page usually means Under Attack Mode or a JS challenge is active on the site. This is a site-wide setting the origin owner controls — API paths need to be explicitly excluded from it.
Can I bypass Cloudflare bot protection for legitimate automated traffic? Yes, through legitimate mechanisms: API tokens, IP allowlisting, mTLS client certificates, or Cloudflare’s own verified bot categories — not through spoofing browser signatures, which is unreliable and breaks unpredictably.
Why did my integration suddenly break with no code changes on my end? The origin site likely changed a Cloudflare security setting (enabled Bot Fight Mode, raised the Security Level, or added a WAF rule) without notifying integration partners. Check the specific error code first to confirm which layer changed.
Does rate limiting count differently for authenticated API requests? It can — many Cloudflare rate limiting rules can be scoped to check headers or cookies, so an authenticated request path can have a separate, usually higher, limit than anonymous traffic. This depends entirely on how the zone owner configured it.
Final Thoughts
The fastest path through a Cloudflare API block is resisting the urge to guess. Pull the actual status code and cf-ray header first, match it against what layer is responsible, and fix that specific layer — a scoped WAF exception, a bot-management adjustment, or just respecting a rate limit with proper backoff. Almost every “Cloudflare is randomly blocking my API” situation I’ve hit turned out to be one specific, identifiable rule doing exactly what it was configured to do.
If this kind of infrastructure debugging is useful to you, I cover more DevOps, cloud, and backend troubleshooting like this regularly on SpiritCode.blog — worth digging through if you want the reasoning behind the fix, not just the fix itself.

