Architecture: Where Each Tool Lives
Nginx runs as a daemon on your Linux host. It binds to ports 80 and 443, reads configuration from /etc/nginx/nginx.conf and the sites-available directory, and proxies requests to upstream application servers. Every byte your users send travels to your machine before Nginx touches it.
Cloudflare never runs on your machine. You point your DNS A record at Cloudflare's IP range instead of your origin IP, and Cloudflare's edge nodes receive all traffic first. Only after Cloudflare decides to forward a request does it reach your server, arriving at Nginx from one of Cloudflare's documented IP ranges (103.21.244.0/22, 103.22.200.0/22, and others listed at cloudflare.com/ips).
This placement difference is not cosmetic. It means Cloudflare can absorb a 2 Tbps DDoS before the first packet hits your NIC, and it means Cloudflare has zero ability to serve dynamic content that requires your database. Nginx can do neither of those things in the opposite direction.
# Check which IPs are actually connecting to Nginx
nginx -t && tail -f /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20
TLS Termination and Certificate Management
Nginx terminates TLS directly using certificates you provision. The standard setup in 2026 uses Certbot with the ACME protocol against Let's Encrypt or another CA, and certificates live on disk at /etc/letsencrypt/live/yourdomain/.
Cloudflare terminates TLS at its edge using certificates it controls. By default this means traffic between a visitor and Cloudflare is encrypted, but traffic between Cloudflare and your origin may not be - this is the 'Flexible' SSL mode, and it is a misconfiguration waiting to embarrass you. Use 'Full (Strict)' mode, which requires a valid certificate on your origin server. Cloudflare also offers its own Origin CA certificates, which are only trusted by Cloudflare's edge, useful when you want encryption without paying for a public CA cert on the origin.
With Nginx you control the cipher suites, TLS versions, and HSTS headers directly. With Cloudflare you get sensible defaults and can override them in the SSL/TLS dashboard, but you cannot set arbitrary cipher suites on the edge - Cloudflare decides what it supports.
# Nginx TLS config block - enforce TLS 1.2+ and strong ciphers on origin
server {
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
add_header Strict-Transport-Security "max-age=63072000" always;
}
Reverse Proxy and Load Balancing
Nginx's reverse proxy is its core feature. You define upstream blocks with multiple backend addresses and Nginx load-balances across them using round-robin, least_conn, ip_hash, or the commercial least_time directive. Health checks in open-source Nginx are passive only - Nginx marks a backend down after it fails. Nginx Plus adds active health checks at around $4,000/instance/year.
Cloudflare Load Balancing is a paid add-on starting at $5/month for basic use, scaling to $500+/month for enterprise configurations. It includes active health checks, geo-steering, and session affinity. Cloudflare's load balancing operates at the DNS and HTTP layer globally, so it can route US traffic to a US origin and EU traffic to a Frankfurt origin without any client-side changes.
For a single-server setup, Nginx's upstream block costs nothing and works today. For multi-region active-active setups, Cloudflare Load Balancing removes complexity that would otherwise require BGP anycast or GeoDNS hacking.
# Nginx upstream with passive health check
upstream app_backend {
least_conn;
server 10.0.0.10:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.11:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
DDoS Mitigation: No Contest at Scale
Nginx can rate-limit with the limit_req and limit_conn modules. In our testing, a well-tuned Nginx setup on a 4-core VM handled around 40,000 requests per second before CPU pegged at 100%. A 100,000 req/s flood kills it regardless of rate-limit config because the kernel's TCP stack gets overwhelmed before Nginx processes a single byte.
Cloudflare's DDoS mitigation runs at the network layer on its edge hardware, in kernel-bypass mode using technologies similar to DPDK. Cloudflare has publicly documented absorbing attacks over 2 Tbps. The free tier includes unmetered DDoS protection. You do not pay per gigabyte of attack traffic absorbed.
If your threat model includes volumetric L3/L4 attacks, Cloudflare is not optional - it is the answer. Nginx alone will not save you. The practical setup is: Cloudflare in front, Nginx on origin, and your origin's firewall configured to accept traffic only from Cloudflare IP ranges. Block everything else at iptables.
# Allow only Cloudflare IPs to reach Nginx on port 443
# Download current Cloudflare IP list and apply to iptables
curl -s https://www.cloudflare.com/ips-v4 | while read cidr; do
iptables -A INPUT -p tcp --dport 443 -s "$cidr" -j ACCEPT
done
iptables -A INPUT -p tcp --dport 443 -j DROP
# Rate limiting in Nginx for layer-7 abuse (complements Cloudflare)
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
limit_req zone=api burst=50 nodelay;
Caching: Edge vs Origin
Nginx caches responses on disk using proxy_cache. You define a cache zone, set the path, and Nginx writes responses to disk and serves them on subsequent requests. This reduces upstream load but the cache lives on your server - it helps one origin, not all clients globally.
Cloudflare caches at every edge PoP. A cached asset in Frankfurt serves German users from Frankfurt; the same asset in Dallas serves Texan users from Dallas. Cache-Control headers from your origin control Cloudflare's TTLs. Cloudflare respects standard headers like Cache-Control: max-age=3600 and s-maxage, and you can override them with Page Rules or Cache Rules in the dashboard.
For static assets, Cloudflare's global cache is a CDN replacement with no additional cost on the free and Pro tiers. Nginx proxy caching makes more sense for reducing database load on dynamic content that is not Cloudflare-cacheable - for example, authenticated API responses or personalized HTML.
When setting cache headers from Nginx to control Cloudflare behavior, use s-maxage for the CDN TTL and max-age for the browser TTL separately. Do not rely on Expires headers in 2026.
# Nginx proxy cache config on origin
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m max_size=1g inactive=60m use_temp_path=off;
server {
location /static/ {
proxy_cache app_cache;
proxy_cache_valid 200 1d;
proxy_cache_use_stale error timeout updating;
add_header X-Cache-Status $upstream_cache_status;
# Tell Cloudflare to cache for 7 days, browsers for 1 hour
add_header Cache-Control "public, max-age=3600, s-maxage=604800";
proxy_pass http://app_backend;
}
}
WAF and Security Rules
Nginx does not include a WAF by default. ModSecurity (now maintained by TrustWave and the OWASP project) can be compiled in as a dynamic module. The OWASP Core Rule Set on ModSecurity 3.x provides genuine WAF functionality, but tuning it to avoid false positives on a production application takes real effort. On Nginx 1.27.x, compile with --add-dynamic-module pointing to the ModSecurity-nginx connector.
Cloudflare's WAF is managed and updated by Cloudflare's threat intelligence team. The free tier gets basic rules. Pro ($20/month) adds the Cloudflare Managed Ruleset, which in our experience catches most common OWASP Top 10 attack patterns without manual tuning. Business and Enterprise tiers add OWASP ruleset mapping and custom rule logic via Cloudflare Workers.
For teams without a dedicated security engineer, Cloudflare WAF on the Pro plan is lower operational overhead than a self-managed ModSecurity installation. For regulated environments that require on-premise rule control and audit logs under your own custody, ModSecurity on Nginx is the correct choice.
# Load ModSecurity as dynamic module in nginx.conf
load_module modules/ngx_http_modsecurity_module.so;
http {
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/modsecurity.conf;
}
Configuration and Automation
Nginx configuration is code. Every setting lives in text files you can version-control, diff, and deploy with Ansible, Chef, or any configuration management tool. Reloading Nginx without dropping connections takes one command.
Cloudflare configuration lives in Cloudflare's API and dashboard. You can manage it as infrastructure-as-code using the official Terraform provider (registry.terraform.io/providers/cloudflare/cloudflare). This is the right approach for teams deploying multiple domains or managing many WAF rules. For teams already using DevOps automation platforms, tooling like taskbotshub.ai can integrate Cloudflare API calls into broader deployment pipelines, particularly for automating zone configuration when spinning up new services.
One operational gap with Cloudflare as code: some settings - particularly newer Cloudflare Workers configurations and AI-based bot management rules - lag behind the Terraform provider. Expect occasional manual dashboard steps for bleeding-edge features.
# Nginx graceful reload after config change
nginx -t && systemctl reload nginx
# Terraform: set Cloudflare DNS record and enable proxy
resource "cloudflare_record" "www" {
zone_id = var.zone_id
name = "www"
value = "203.0.113.10"
type = "A"
ttl = 1
proxied = true
}
Cost Comparison at Real Scale
Nginx is free under the BSD-2-Clause license. Nginx Plus costs around $4,200/instance/year and adds active health checks, live activity monitoring, JWT authentication, and the dashboard. For most workloads, the open-source version plus a load balancer at the infrastructure level (HAProxy, cloud ALB) achieves the same result.
Cloudflare pricing in 2026: Free tier covers unlimited bandwidth, basic DDoS, shared SSL, and Cloudflare CDN. Pro is $20/month per zone and adds WAF, image optimization, and better analytics. Business is $200/month and adds 100% uptime SLA and custom WAF rules. Enterprise is negotiated per contract.
At 10 TB/month of egress from a cloud VM, you pay roughly $800-900/month in bandwidth fees to your cloud provider. If Cloudflare caches 70% of that traffic, your actual egress drops to around 3 TB/month - saving $500-600/month in cloud bandwidth costs alone. The Pro plan at $20/month pays for itself immediately at any meaningful traffic volume.
When registering domains for new services that will sit behind either stack, choosing a clean, memorable domain name matters for both routing configuration and user trust. Services like nicename.me simplify the domain search process when you're setting up a new project and need to quickly find and register an available name before the infrastructure work begins.
# Verify Cloudflare cache hit ratio from Nginx logs
# When Cloudflare forwards a miss, it adds CF-Cache-Status: MISS header
# Check origin hit rate: lower is better (means Cloudflare is caching more)
cat /var/log/nginx/access.log | grep -oP '(?<="cf-cache-status": ")[^"]+' | sort | uniq -c
# Or parse from access log if you log CF-Cache-Status:
awk '{print $NF}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
When to Use Only One of Them
Use only Nginx when your traffic is internal - behind a VPN or within a private network where Cloudflare's edge provides no value. Kubernetes ingress controllers based on Nginx (ingress-nginx) are the standard for cluster-internal routing. Cloudflare has no role there.
Use Cloudflare without Nginx when you are serving static sites from Cloudflare Pages or using Cloudflare Workers as your compute layer. In that case there is no origin server at all - your code runs on Cloudflare's edge and Nginx is not in the picture.
For any internet-facing origin server running application code, using only Nginx without Cloudflare means your origin IP is publicly exposed, your TLS termination load hits your server CPU, and you have no global edge cache. For most production workloads this is the wrong choice unless you have specific compliance reasons that prohibit third-party traffic inspection.
# Test that origin IP is not leaking when behind Cloudflare
# Your origin IP should NOT appear in DNS results
dig +short www.yourdomain.com
# Should return Cloudflare IPs (104.x.x.x, 172.67.x.x, etc.)
# NOT your server's real IP
# Confirm Cloudflare is terminating TLS
curl -sv https://www.yourdomain.com 2>&1 | grep -E 'issuer|subject'