Load Testing a Site Behind Cloudflare

Load testing a site behind Cloudflare usually fails in one of two ways: the test traffic is blocked or challenged (HTTP 403, 429, challenge pages) because a few IPs sending thousands of requests look like a bot, or it succeeds against Cloudflare's cache and never measures your origin. Decide first whether you are testing the edge or the origin. Then either allowlist the load generator's IPs with a WAF custom rule using the Skip action, or test the origin directly. Check cf-cache-status on responses to see what you actually measured.
What goes wrong when you test through Cloudflare
Run a load test against a Cloudflare-proxied hostname and a few seconds in you typically see one or more of these:
| What the generator records | What it means |
|---|---|
403 with header cf-mitigated: challenge, HTML body with a challenge page | A managed or JS challenge was issued. Load generators cannot solve it, so every request "fails" quickly. |
| 403, Cloudflare block page, error 1020 (Access denied) | A WAF custom rule or managed rule blocked the request. |
| 429, Cloudflare page with error 1015 (You are being rate limited) | A rate limiting rule matched the generator's IPs. |
Very low latency, near-zero origin CPU, cf-cache-status: HIT | You are measuring Cloudflare's cache, not your application. |
| 520-526 errors | Cloudflare reached for your origin and failed. These are real origin problems (see the table below). |
The first three mean the test is invalid: the error rate is Cloudflare protecting you, and the latency of challenge pages tells you nothing. The fourth means the test is valid for the edge but says nothing about origin capacity. Only the 52x errors are real findings about your stack.
The security features that usually fire are Bot Fight Mode (or Super Bot Fight Mode on paid plans), rate limiting rules, WAF managed rules, Browser Integrity Check (which flags clients with missing or unusual headers), Security Level, and, if someone enabled it, Under Attack mode, which challenges every visitor.
Decide what you are testing: edge or origin
These are two different questions, and mixing them produces numbers that answer neither.
| Approach | What it measures | Use it when | Watch out for |
|---|---|---|---|
| Through Cloudflare, cache as configured | Real user experience including cache hit ratio | Pre-launch check of the full path, cache rules validation | Mostly measures the CDN; origin may barely be touched |
| Through Cloudflare, uncacheable requests | Origin capacity plus the edge path, TLS to origin, WAF overhead | APIs, logged-in pages, checkout flows | Needs a WAF skip rule for generator IPs |
| Directly to origin, bypassing Cloudflare | Origin capacity alone | Capacity planning, finding the app's knee | Origin firewall may only allow Cloudflare IPs; no edge effects |
For capacity planning, testing the origin is almost always what you want. Your origin will see the cache-miss traffic in production, and the question is how much of it it can handle. Testing the edge is useful once, to confirm the cache behaves the way you think, but Cloudflare's network will not be your bottleneck.
You might be testing the cache
Every proxied response carries a cf-cache-status header. Sample a few responses before the test, and log the header during it if your tool allows:
curl -sI https://www.example.com/ | grep -i cf-cache-status
curl -sI https://www.example.com/assets/app.js | grep -i cf-cache-status
HIT: served from cache. The origin was not involved.MISS/EXPIRED: fetched from origin this time, will be cached.DYNAMIC: not eligible for caching, always goes to origin. By default Cloudflare caches static file extensions but not HTML, so normal HTML pages and API responses usually show this.BYPASS: a rule or origin header (for exampleCache-Control: privateor a cookie) told Cloudflare not to cache.
If your test targets cached assets, you are measuring Cloudflare. Cross-check with origin metrics: if the generator reports 2,000 RPS and the origin access log shows 40, the cache absorbed 98% of the load.
Cache-busting, carefully
Appending a random query string (?cb=8f3a1) forces a miss under Cloudflare's default cache key, which includes the query string. It works, but use it deliberately:
- It models a 0% hit ratio, which is harsher than production. Fine for a stress test, misleading if reported as "expected peak performance".
- It fills the edge cache with entries nobody will request again.
- If your zone ignores or sorts query strings in the cache key, it does nothing; check
cf-cache-statusshowsMISS.
A more realistic approach is to mix: a weighted scenario where most requests hit cacheable URLs and a share (matching your production hit ratio from Cloudflare analytics) are uncacheable or busted.
Allowlist the load generator in the WAF
To test through the edge without being challenged or rate limited, create a WAF custom rule that matches the generator's source IPs and uses the Skip action. LoadTester publishes its worker IPs at https://app.loadtester.org/api/ips; other tools and cloud generators have their own published ranges, or you use the egress IPs of your own machines.
- Create an account-level IP list (under Lists in your Cloudflare account settings) containing the generator IPs, so the rule stays readable and you can update addresses in one place. Or put the addresses inline.
- In the zone's WAF custom rules, create a rule with an expression like:
(ip.src in $loadtest_generators and http.host eq "www.example.com") # or inline (ip.src in {192.0.2.10 192.0.2.11 198.51.100.0/24}) - Action: Skip. Select the components to skip: remaining custom rules, rate limiting rules, managed rules, and Super Bot Fight Mode, plus other listed features such as Browser Integrity Check and Security Level if they are firing.
- Place the rule first in the custom rules order, and turn on logging for it so you can see it matching in Security Events.
Things the skip rule does not cover:
- Bot Fight Mode (the free-plan feature) cannot be skipped with WAF custom rules. If it is on, turn it off for the duration of the test, or test the origin directly. Super Bot Fight Mode on paid plans can be skipped.
- HTTP DDoS protection runs separately from the WAF. A very high request rate from a few IPs can still trigger it. Check the DDoS section of Security Events if requests are mitigated after the skip rule is in place.
- Under Attack mode challenges everyone; disable it for the test window.
Treat the rule as temporary. A broad skip for a set of IPs is an open door for anything else that uses those addresses. Scope it by hostname, remove or disable it after the test, and never use it for shared cloud IP ranges you do not control. Domain verification on the LoadTester side means only domains you have proven you control can be targeted from those workers, but your skip rule is still yours to keep tight.
Test the origin directly
To bypass the edge, point the generator at the origin while keeping the real hostname for the Host header and TLS SNI:
# quick check from a shell: resolve the hostname to the origin IP
curl -sv --resolve www.example.com:443:203.0.113.20 https://www.example.com/health
# or use a separate DNS-only (grey-cloud) hostname that points at the origin,
# e.g. origin-test.example.com, with a certificate valid for it
Common blockers and what to do about them:
- Origin firewall only allows Cloudflare IP ranges (a recommended setup). Add the generator IPs to the origin firewall or security group for the test window.
- Authenticated Origin Pulls (mTLS between Cloudflare and origin). Direct requests will fail the client-certificate check. Test against a separate origin listener without the requirement, or test through the edge.
- Cloudflare Tunnel. The origin has no public listener, so the only path is through the edge. Use the allowlist approach above.
- Origin certificate is a Cloudflare Origin CA certificate, which public clients do not trust. Use a hostname with a publicly trusted certificate, or disable verification in the tool only for this test.
When testing the origin directly, remember that it no longer sees Cloudflare's headers (CF-Connecting-IP), so logic that relies on them (rate limits by client IP, geolocation) behaves differently.
Cloudflare 520-527 errors explained
When requests do get through, the 52x codes are the most useful signals, because they describe what happened between Cloudflare and your origin:
| Code | Name | Meaning under load |
|---|---|---|
| 520 | Web server returned an unknown error | Origin closed the connection or sent an empty/invalid response. Same root causes as a 502 under load: crashes, keep-alive races, oversized headers. |
| 521 | Web server is down | Origin refused the TCP connection: process down, or the origin firewall blocking Cloudflare. |
| 522 | Connection timed out | TCP handshake to origin did not complete. Overloaded origin, full SYN/accept backlog, or packets dropped by a firewall. |
| 523 | Origin is unreachable | Routing or DNS problem to the origin. Rarely load-related. |
| 524 | A timeout occurred | Connected, but no HTTP response within 100 s (default proxy read timeout). Slow requests; see 504 gateway timeout under load. |
| 525 | SSL handshake failed | TLS handshake with origin failed. Under load, often an origin running out of TLS workers or connections. |
| 526 | Invalid SSL certificate | Origin certificate invalid in Full (strict) mode. Configuration, not load. |
| 527 | Railgun error | Railgun connection failure. Railgun has been discontinued, so you should not see this on a current setup. |
A 504 seen through Cloudflare was generated by something behind it (your ALB or nginx), not by Cloudflare.
AWS WAF, CloudFront and Akamai
The same two problems (blocked traffic and cached responses) apply to every CDN and WAF. The specifics differ; the approach does not.
AWS WAF and CloudFront
- Blocking: rate-based rules and the Bot Control managed rule group are the usual triggers, returning 403 by default. Add an IP set with the generator addresses and a rule with the Allow action at a higher priority (lower number) than the rate-based and managed rules. Allow is a terminating action, so later rules are not evaluated for those requests.
- Caching: CloudFront reports cache results in the
X-Cacheheader (Hit from cloudfront,Miss from cloudfront). The same edge-vs-origin decision applies; to test origin capacity, target the ALB or origin directly. - Policy: AWS publishes policies covering penetration testing and simulated DDoS events. A normal load test of your own application is not a DDoS simulation, but read the current policy before very large tests through CloudFront.
Akamai
Akamai security configurations include rate controls and bot management that will flag a load generator the same way. Add the generator IPs to an allowlist (Akamai manages IP allowlists as network lists) and exempt it from rate and bot controls in the security policy, or coordinate with whoever manages your Akamai configuration. For caching, check the cache-status debug headers your configuration exposes, or confirm against origin request counts.
Notify people and check the terms
- Tell your team and your provider's account contacts if the test is large. A sudden traffic spike can page the on-call engineer and trigger automated mitigation.
- Read your CDN plan's terms. Testing your own zone is normal, but providers expect large tests to be coordinated, and enterprise contracts often have a process for it.
- Test only what you own. Never load test a third-party domain behind a CDN, even if it is a dependency of your site. LoadTester enforces this through domain verification (a DNS TXT record or a file at
/.well-known/loadtester-verify.txt). - Schedule it. Run large edge tests outside peak hours so real users are not caught by temporary rule changes. The production load testing checklist covers the rest.
Diagnosis table
| Symptom / signal | Likely cause | Fix |
|---|---|---|
403 with cf-mitigated: challenge | Bot protection or Security Level challenge | WAF Skip rule for generator IPs; turn off Bot Fight Mode for the test |
| 403, error 1020 | WAF custom or managed rule block | Skip rule; check Security Events for the rule ID |
| 429, error 1015 | Rate limiting rule | Skip rate limiting rules for generator IPs |
| Still mitigated after skip rule | Bot Fight Mode or HTTP DDoS protection | Disable Bot Fight Mode; check DDoS events; lower the rate or test origin |
Latency tiny, origin idle, cf-cache-status: HIT | Testing the cache | Test uncacheable paths, cache-bust a share, or go direct to origin |
| 520 under load | Origin closing connections / crashing | Debug as a 502 |
| 521 / 522 under load | Origin refusing or not accepting connections | Origin capacity, backlog, firewall |
| 524 | Origin response took > 100 s | Debug as a 504; move long work async |
| Direct-to-origin test gets TLS errors | Origin CA cert or Authenticated Origin Pulls | Use a publicly trusted hostname or a test listener |
Re-test and confirm the setup
Before the real run, do a short validation run at low load (for example 20 RPS for 1 minute) and check three things:
- No mitigation: zero 403/429 responses, and Security Events shows your skip rule matching the generator IPs (or no events at all for a direct-to-origin test).
- Correct cache behavior:
cf-cache-statusvalues match your intent, and the origin access log shows the request rate you expect it to receive. - Origin metrics move: origin CPU and request count rise with the test. If they do not, you are not testing what you think.
Then run the actual test as a step-ramp in fixed-RPS mode, and watch the 52x codes alongside p95 and p99. A rising 520/522 rate is your origin's capacity limit showing through the edge, which is exactly the finding the test was meant to produce. For how to set the steps and interpret the curve, see why latency increases under load and how to read load test results. Finally, remove or disable the skip rule and restore Bot Fight Mode or any other settings you changed.
FAQ
Will Cloudflare block my load test?
Often, yes. A high request rate from a few IP addresses matches the patterns that Bot Fight Mode, rate limiting rules and managed WAF rules are designed to stop, so you get 403 challenges or 429 responses. Add a WAF custom rule with the Skip action for the generator IPs, or test the origin directly.
Can I skip Bot Fight Mode with a WAF custom rule?
No. The free-plan Bot Fight Mode cannot be bypassed with the Skip action, so it has to be turned off for the test window or the origin tested directly. Super Bot Fight Mode on paid plans can be skipped with a custom rule.
Should I load test through Cloudflare or against the origin?
For capacity planning, test the origin, because that is the part of the system that can run out of capacity. Testing through the edge is useful to validate cache rules and the full request path, but with a high cache hit ratio it mostly measures Cloudflare.
How do I know if my load test is hitting Cloudflare's cache?
Check the cf-cache-status response header. HIT means the response came from cache without touching the origin; DYNAMIC and BYPASS mean the request went to the origin. Comparing the generator's request rate with the origin access log shows how much the cache absorbed.
What does a Cloudflare 522 mean during a load test?
Cloudflare could not complete a TCP connection to your origin in time. Under load this usually means the origin is overloaded, its listen backlog is full, or a firewall is dropping connections. It is a real origin capacity finding, not a test artifact.