Cloudflare 520, 521, 522, 523, 524: How to Tell Them Apart and Fix Each From the Server Side
If you run sites behind Cloudflare long enough, you will meet the whole 52x family. They all look the same to a visitor: a Cloudflare-branded page saying something is wrong with the "host". But each code tells you someth
If you run sites behind Cloudflare long enough, you will meet the whole 52x family. They all look the same to a visitor: a Cloudflare-branded page saying something is wrong with the "host". But each code tells you something different about where the conversation between Cloudflare and your origin server broke down.
I've spent a lot of years fixing these on WordPress sites, VPSes and shared hosting, and the biggest time saver is simple: read the code carefully before touching anything. A 521 and a 524 have almost nothing in common, and fixing the wrong layer wastes hours.
Here's how I approach them.
The quick comparison
| Code | What Cloudflare saw | First thing to check |
|---|---|---|
| 520 | Connected, but got an empty, invalid or unexpected response | Web server / PHP-FPM error logs, oversized headers |
| 521 | Connection refused by the origin | Is Nginx/Apache running and listening on 80/443? |
| 522 | TCP connection timed out (no answer at all) | Firewall DROP rules, wrong origin IP, server overload |
| 523 | Origin unreachable (no route) | DNS record value, stale AAAA record, host network |
| 524 | Connected, but no HTTP response within ~100 seconds | Slow PHP scripts or database queries |
A useful mental model: 521, 522 and 523 are about getting a connection. 520 and 524 are about what happens after the connection is open.
The universal first step: talk to the origin directly
Before changing anything in Cloudflare or on the server, take Cloudflare out of the picture. curl --resolve lets you send a request for your real hostname straight to your origin IP, with the correct SNI and Host header:
# Replace example.com and 203.0.113.10 with your domain and real origin IP
curl -svo /dev/null https://example.com/ --resolve example.com:443:203.0.113.10
# Same over plain HTTP
curl -svo /dev/null http://example.com/ --resolve example.com:80:203.0.113.10
Run it from a machine outside your server (your laptop is fine). What you see narrows things down fast:
-
Connection refused→ you are looking at a 521 problem. - The command hangs, then times out → firewall or network, think 522/523.
-
Empty reply from serveror a reset mid-response → classic 520. - It works but takes forever → 524 territory.
- It works fine and fast → the origin is healthy from the outside, so suspect something that only affects Cloudflare: a firewall blocking Cloudflare's IP ranges, a wrong IP in the Cloudflare DNS tab, or an SSL mode mismatch.
Now let's go code by code.
520: the origin answered, but badly
520 is Cloudflare's catch-all: the TCP connection worked, but what came back was empty, malformed or cut off. Most of the time the cause is a crashing backend.
Check the services and logs:
sudo systemctl status nginx # or apache2 / httpd
sudo systemctl status php8.2-fpm # match your PHP version
sudo tail -n 50 /var/log/nginx/error.log
sudo grep -iE 'segfault|exited on signal|max_children' /var/log/php*-fpm.log
PHP-FPM workers dying with a segfault, or hitting pm.max_children, are the usual suspects on WordPress sites.
Check header size. Cloudflare rejects responses with very large headers, which often happens when a plugin sets huge cookies:
curl -s -D - -o /dev/null https://example.com/ \
--resolve example.com:443:203.0.113.10 | wc -c
If the number is in the tens of kilobytes, find the cookie responsible. While you do, give Nginx bigger buffers so it doesn't choke:
# nginx.conf, inside http {}
large_client_header_buffers 4 32k;
proxy_buffer_size 16k;
proxy_buffers 8 16k;
A firewall that resets Cloudflare connections partway through can also produce 520, so the allowlist in the 521 section below is worth doing either way.
521: the origin refused the connection
521 is the most straightforward. Cloudflare knocked and the server said no, either because nothing is listening or because something actively rejected it.
Is the web server up and is the config valid?
sudo systemctl status nginx
sudo nginx -t && sudo systemctl restart nginx # Apache: sudo apachectl configtest
Is it listening on the public interface?
sudo ss -tlnp | grep -E ':(80|443)\b'
You want to see 0.0.0.0:80 / 0.0.0.0:443 (or [::]). If it's bound only to 127.0.0.1, Cloudflare can't reach it.
Does the SSL mode match the origin? With Cloudflare SSL set to Full or Full (strict), Cloudflare connects on port 443. If your origin only serves HTTP, every request gets refused:
sudo nginx -T | grep -E 'listen .*443'
sudo apachectl -S | grep 443
Allow Cloudflare's IP ranges. All your traffic arrives from a small set of Cloudflare IPs, which makes them easy targets for Fail2ban, CSF/LFD or rate limits. Allowlist the official ranges:
for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do
sudo ufw allow proto tcp from "$ip" to any port 80,443 comment 'Cloudflare'
done
sudo ufw reload
# Check Fail2ban hasn't banned any of them
sudo fail2ban-client status
On CSF, add each range to /etc/csf/csf.allow and run sudo csf -r.
522: the connection timed out
The difference from 521 is subtle but important: with 522 nobody said no. Packets just disappeared. That points to a firewall that drops instead of rejecting, the wrong IP, or a server too busy to accept connections.
Confirm Cloudflare is pointing at the right server. After a migration, a stale A record is the number one cause I see:
# On the server: its real public IPv4
curl -4 -s https://ifconfig.me
# From outside: does that IP answer on 443 at all?
nc -vz -w 5 203.0.113.10 443
Compare that IP with the A record in the Cloudflare DNS tab. Note that dig example.com will show Cloudflare's IPs for proxied records, which is expected and tells you nothing about the origin.
Look for DROP rules and connection limits:
sudo iptables -L INPUT -n --line-numbers | grep -E 'DROP|REJECT|connlimit'
Check whether the server is simply overloaded:
uptime
free -m
sudo ss -s
sudo ss -tn state syn-recv | wc -l
A big pile of connections stuck in SYN-RECV means the server isn't keeping up. On Nginx, make sure workers aren't the bottleneck, and keep keep-alive on, because Cloudflare reuses connections:
worker_processes auto;
events { worker_connections 4096; }
http {
keepalive_timeout 75s;
}
523: the origin is unreachable
523 means Cloudflare couldn't even find a route to your origin. It's almost always DNS or the hosting provider's network, rarely your web server config.
Check both IPv4 and IPv6. A forgotten AAAA record is a sneaky cause: Cloudflare may try IPv6 and fail, while everything looks fine over IPv4.
curl -4 -s https://ifconfig.me
curl -6 -s https://ifconfig.me # if this fails, remove the AAAA record in Cloudflare
Trace the route from outside:
mtr -rwc 50 203.0.113.10
# or
traceroute 203.0.113.10
If the trace dies before reaching your server, the problem is upstream. Save the output and send it to your host; it's exactly what their network team will ask for.
Make sure the server's own network is up:
ip -br addr
ip route show default
No default route after a reboot or a netplan change will produce 523 just as reliably as a dead data center.
524: the origin was too slow
With 524, everything connected fine, but the origin didn't send a response before Cloudflare's timeout (100 seconds on the Free, Pro and Business plans). You can't raise that limit by tuning your server; you have to make the request faster or move it out of the proxied path.
Measure the slow URL directly:
curl -so /dev/null \
-w 'connect=%{time_connect}s ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
https://example.com/slow-page/ --resolve example.com:443:203.0.113.10
A ttfb near 100 seconds is your answer.
Find what's slow. Turn on the PHP-FPM slow log and the MySQL slow query log:
; /etc/php/8.2/fpm/pool.d/www.conf
request_slowlog_timeout = 10s
slowlog = /var/log/php-fpm-slow.log
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 2;
The slowlog gives you a PHP stack trace for every request over 10 seconds, which usually points straight at the plugin or query to blame.
Move long jobs out of the request. Imports, exports, backups and reports shouldn't run inside a web request. On WordPress, run cron from the system instead of on page load:
// wp-config.php
define('DISABLE_WP_CRON', true);
# crontab -e
*/5 * * * * cd /var/www/html && wp cron event run --due-now >/dev/null 2>&1
For admin tools that genuinely need minutes, put them on a subdomain that isn't proxied (grey cloud) in Cloudflare.
A short checklist to keep around
- Read the exact code. Refused (521), timed out (522), unreachable (523), bad response (520) and slow response (524) are different problems.
- Run
curl --resolveagainst the origin from outside before changing anything. - Allowlist Cloudflare's IP ranges in every firewall layer you have.
- Verify the origin IP (and any AAAA record) in the Cloudflare DNS tab.
- Check logs before restarting services. A restart hides the evidence.
Full step-by-step guides
Each of these codes has more causes and edge cases than fit in one post. I keep detailed, regularly updated guides for each on ErrorRescue:
- Cloudflare Error 520: Web Server Returned an Unknown Error
- Cloudflare Error 521: Web Server Is Down
- Cloudflare Error 522: Connection Timed Out
- Cloudflare Error 523: Origin Is Unreachable
- Cloudflare Error 524: A Timeout Occurred
If you've hit a 52x case that didn't fit any of these patterns, I'd like to hear about it in the comments.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.