Skip to main content

DDoS attack protection: Nginx and Cloudflare

Security · 19.04.2026 · 4 min read
Illustration for “DDoS attack protection: Nginx and Cloudflare”

DDoS attacks (Distributed Denial of Service) overload a server with a massive volume of requests, making it unreachable for regular visitors. Effective protection is built in layers: network level (Cloudflare/CDN), system level (sysctl) and application level (Nginx) — no single layer on its own covers every attack scenario.

Cloudflare absorbs DDoS traffic before it reaches your server, because all traffic passes through its network of points of presence:

  • Turn on Proxy (the orange cloud) for the A and CNAME DNS records — without it Cloudflare only sees DNS lookups while the actual traffic goes straight to the server.
  • Security → DDoS → enable HTTP DDoS Attack Protection.
  • Security → WAF → enable Managed Rules.
  • Security Level: High during an attack, Medium in normal operation.

Layer 2: Nginx connection limits (on top of UFW Firewall)

http {
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
    limit_req_zone $binary_remote_addr zone=req_limit:10m rate=50r/s;

    server {
        limit_conn conn_limit 20;      # Max 20 connections from one IP
        limit_req zone=req_limit burst=100 nodelay;
        limit_req_status 429;

        # Timeouts against Slowloris
        client_body_timeout 10;
        client_header_timeout 10;
        keepalive_timeout 5;
        send_timeout 10;
    }
}

Layer 3: System tuning (sysctl)

# /etc/sysctl.d/99-ddos-protect.conf
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 5
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_keepalive_time = 300
net.ipv4.conf.all.rp_filter = 1

sysctl --system

Reading attack signals in the logs

  • 499 in the Nginx log — the client dropped the connection before getting a response; a mass spike of 499s points to a SYN flood or a saturated worker queue, not a slow application.
  • upstream timed out (110: Connection timed out) — during an application-layer (L7) attack, PHP-FPM workers are busy processing forged requests and cannot keep up with real users.
  • too many open files in dmesg or the Nginx log — the file descriptor limit is exhausted by thousands of simultaneous connections; raising ulimit -n is a stopgap, not a fix for the attack itself.
  • Resource temporarily unavailable (11: Resource temporarily unavailable) on accept() — the queue of new connections is full, the server physically cannot accept new sockets faster than the attacker creates them.

Responding to an attack

netstat -tn | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -20
ss -s

# Quickly block the attacking IP
iptables -I INPUT -s ATTACK_IP -j DROP

# Turn on "Under Attack" mode in Cloudflare
# Security, Settings, Security Level: I'm Under Attack

Hiding the IP behind Cloudflare: make sure the server's real IP is not exposed through DNS history, email headers or other channels. Cloudflare is only effective while the attacker does not know the real IP and cannot hit it directly, bypassing the protection.

Comparing the protection layers

LayerWhat it protects againstWhere it is configuredRequires a server reboot
CloudflareVolumetric (L3/L4) and part of L7 attacksCloudflare dashboardNo
NginxL7: flooding requests to one URLVirtual host configNo (reload)
sysctlL4: SYN flood, socket exhaustionOS kernelNo (sysctl --system)

When this set of measures is not enough

  • The attack hits the server's IP directly, bypassing Cloudflare — if the IP leaked earlier, DNS proxying will not save you; you need to change the IP and keep it secret, or move the server into a provider's protected network.
  • The attack runs at hundreds of gigabits per second — the server's own uplink physically cannot carry that traffic even with perfect Nginx rules; only upstream filtering at the provider or CDN level solves this.
  • The attack mimics real users through rotating residential proxies — a simple per-IP limit cannot tell them apart from real people; you need behavioral WAF rules and a captcha on suspicious sessions.

How to verify the protection is working

# Confirm requests really reach Cloudflare instead of going around it
dig +short example.com
# Expect an IP from Cloudflare's ranges, not your server's IP

# Check that the rate limit triggers on Nginx itself
for i in $(seq 1 60); do curl -o /dev/null -s -w "%{http_code}\n" https://example.com/; done | sort | uniq -c

# Confirm the sysctl parameters were applied
sysctl net.ipv4.tcp_syncookies

If DNS returns an IP from Cloudflare's range, part of the requests during a load test come back as 429, and sysctl shows the values you set, all three protection layers are working together.

Was this article helpful?
← Back to Knowledge Base Ask Support