Skip to content
Control Plane Labs

521 Web Server Is Down

Server error response defined by developers.cloudflare.com.

Last updated July 27, 2026

Common causes at a glance

  • The web server process is stopped, crashed, or failed to restart after a deploy
  • A firewall or security plugin rejecting Cloudflare IP ranges with REJECT
  • The server is listening on the wrong port for the zone's SSL/TLS mode
  • The service bound to 127.0.0.1 only, so external connections are refused

Reproduce it

curl -sS --connect-timeout 5 --resolve www.example.com:443:203.0.113.10 https://www.example.com/

What 521 Web Server Is Down means

The distinction between 521 and 522 is the whole point of having two codes, and it maps onto two network behaviours. 521 is ECONNREFUSED: your host received the SYN and answered with a RST. Something is home, it just said no. 522 is silence — the SYN vanished. Refusal is a dead process or a REJECT rule; silence is a DROP rule.

Cloudflare adds a wrinkle: the port it dials depends on your SSL/TLS mode. Flexible connects to port 80, Full and Full (strict) to 443. Switching mode without opening the matching port produces an instant 521 on an otherwise healthy server.

Where it comes from

521 has no RFC. It does not appear in the IANA registry and is defined only by Cloudflare’s documentation. Under RFC 9110 §15 a client that does not recognise 521 treats it as 500, which is why some tools report a generic server error instead. Because the edge synthesises this response after its connection was rejected, your origin never processed a request and never logged one. People waste real time grepping application logs for a 521 that by definition cannot be there; the evidence is in firewall counters and process state.

What actually causes it

The banal cause is the most common: the application is not running. A failed deploy, an OOM kill, or a unit that did not come back after a reboot all present identically. The second cause is Cloudflare-specific and recurs forever, because Cloudflare’s IP ranges change occasionally and hardcoded allowlists go stale. Anything keeping its own list — a WAF appliance, an .htaccess file, fail2ban, a security group — will eventually reject a range added after someone copied it. The bind-address trap catches containerised apps: a service on 127.0.0.1:8080 works in a local shell and refuses everything from outside.

How to debug it

Confirm the refusal from a machine outside the network. A refused connection fails in milliseconds, which is itself the diagnostic — a hang means you are looking at 522, not 521:

curl -sv --connect-timeout 5 --resolve www.example.com:443:203.0.113.10 \
https://www.example.com/ 2>&1 | grep -E 'Connected|refused|Failed'

On the origin, check what is bound where with ss -ltnp and read the address column carefully — a 127.0.0.1 binding is the answer more often than people expect. Then dump firewall rules looking for a stale Cloudflare allowlist, and confirm the record is actually proxied and points at the host you are testing. The DNS tool shows what it resolves to today.

Working examples

curl

# A refusal returns almost instantly. A hang means 522, not 521.
curl -sv --connect-timeout 5 \
  --resolve www.example.com:443:203.0.113.10 \
  https://www.example.com/ 2>&1 | grep -E 'Connected|refused|Failed'

# Check both ports: Flexible mode dials 80, Full and Full (strict) dial 443.
nc -vz 203.0.113.10 80
nc -vz 203.0.113.10 443

Python (requests)

import requests
from requests.exceptions import ConnectionError

# Reproduce the refusal the edge is seeing, straight at the origin IP.
try:
    requests.get(
        "https://203.0.113.10/",
        headers={"Host": "www.example.com"},
        verify=False,
        timeout=5,
    )
except ConnectionError as exc:
    # "Connection refused" -> 521. A read timeout instead -> 522.
    print("refused:", "refused" in str(exc).lower())

# What the visitor actually gets back through Cloudflare.
print(requests.get("https://www.example.com/", timeout=10).status_code)

Node (fetch)

// fetch() surfaces the edge's 521 as a normal response.
const res = await fetch("https://www.example.com/");
console.log(res.status); // 521

// Probe the origin directly to classify the failure.
import net from "node:net";

const sock = net.connect({ host: "203.0.113.10", port: 443 });
sock.setTimeout(5000);
sock.on("connect", () => { console.log("port open — look elsewhere"); sock.destroy(); });
sock.on("timeout", () => { console.log("silent drop — this is a 522"); sock.destroy(); });
sock.on("error", (e) => console.log(e.code)); // ECONNREFUSED -> 521

Reference and tooling

The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.

Other 5xx codes