The classic problem: http://example.com → https://example.com → https://www.example.com → https://www.example.com/page resolves in three hops. Each hop adds 100-500ms latency. Each leaks tiny amounts of link equity. The fix is collapsing to a single hop that goes directly to the final canonical, then enabling HSTS so browsers skip the redirect entirely on repeat visits.
curl -IL http://example.com 2>&1 | grep -E "^HTTP|^location:|^Location:"Should show 1-2 status code lines maximum. If 3+, you have a chain to collapse.
Best practice: HTTPS upgrade happens at the edge (CDN), once, sending directly to the final canonical URL.
Rules → Redirect Rules → Create rule
When: HTTP scheme equals "http"
Then: Static redirect
Target URL: https://www.example.com${http.request.uri}
Status code: 301
Preserve query string: enabled
This single rule handles:
- HTTP → HTTPS upgrade
- non-www → www canonicalisation
- Path preservation
- Query string preservation
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
# non-canonical https → canonical https
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name www.example.com;
# actual content serving
root /var/www/html;
}
# Force HTTPS + www in one rule
RewriteEngine On
RewriteCond %{HTTPS} !=on [OR]
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]
# Check for redundant rules in: # - .htaccess (if Cloudflare handles HTTPS) # - WordPress functions.php (force_ssl_admin etc) # - Application middleware (Express/Django HTTPS forcing) # - Load balancer health check configs # - DNS A records pointing to non-CDN IPs
HSTS tells browsers "use HTTPS for this domain for the next N seconds". After first visit, the browser internally rewrites http:// to https:// — no redirect needed.
server {
listen 443 ssl;
server_name www.example.com;
# Initial conservative HSTS: 1 hour
add_header Strict-Transport-Security "max-age=3600" always;
# After 1 week of HTTPS stability:
# add_header Strict-Transport-Security "max-age=31536000" always; # 1 year
# After 1 month confident:
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Ready for preload:
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
SSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS) → Enable Max Age: 12 months Include subdomains: enabled (only if ALL subdomains have HTTPS) Preload: enabled (only if confident — see warning below) No-Sniff: enabled
Once HSTS is stable with long max-age and includeSubDomains, submit to hstspreload.org. Major browsers ship the list, so even first-time visitors skip the HTTP attempt entirely.
Process:
Cloudflare→Browser is HTTPS, Cloudflare→Origin is HTTP. If your origin redirects HTTP→HTTPS based on local scheme, it sees HTTP from Cloudflare and tries to redirect, causing loop.
# Fix: trust X-Forwarded-Proto header instead of $scheme
if ($http_x_forwarded_proto = "http") {
return 301 https://$host$request_uri;
}
# Or: use Cloudflare "Full" mode so origin connection is HTTPS
If "Always Use HTTPS" is enabled AND a custom page rule does the same thing, you may double-redirect. Use one or the other.
curl -IL http://example.com/page 2>&1 | grep -E "^HTTP|^location:" -i # Expected (single hop): # HTTP/1.1 301 Moved Permanently # location: https://www.example.com/page # HTTP/2 200
curl -I https://www.example.com | grep -i "strict-transport" # Should show: strict-transport-security: max-age=...