I’ve seen this bug ship more than once: a team enables HSTS, feels good about “forcing HTTPS,” and then learns the hard way that HSTS does nothing for a user’s very first HTTP visit.
That gap matters.
If you’re deciding between an HTTPS redirect and HSTS, the answer is not “pick one.” You need both. The real question is which one protects the user first, and how to roll them out without breaking login flows, subdomains, or that one forgotten asset host from 2018.
The short answer
The HTTPS redirect comes first in practice.
HSTS comes first after the browser has seen it once.
That sounds contradictory until you look at what actually happens on the wire.
- A user types
example.com - The browser often tries
http://example.comfirst unless the URL is explicitly HTTPS, preloaded, or already remembered - Your server must redirect that HTTP request to HTTPS
- Once the browser reaches HTTPS, your server can send:
Strict-Transport-Security: max-age=31536000; includeSubDomains
- On later visits, the browser upgrades to HTTPS before making the request
So the first visit is protected by the redirect. Repeat visits are protected by HSTS.
A real-world case: the false sense of safety rollout
A SaaS team I worked with had just completed a “security hardening sprint.” They enabled HSTS on the main app and checked the box in their internal tracker.
Their production behavior looked like this:
Before
server {
listen 80;
server_name app.example.com;
root /var/www/app;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/ssl/app/fullchain.pem;
ssl_certificate_key /etc/ssl/app/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000" always;
location / {
proxy_pass http://app_backend;
}
}
Looks decent at first glance. HTTPS is enabled. HSTS is set. But the HTTP vhost is serving the app directly instead of redirecting.
That means:
- First-time visitors can land on HTTP
- Session cookies might be exposed if
Secureisn’t set everywhere - Links, forms, and mixed assumptions can keep users on HTTP longer than you think
- HSTS is never applied until the browser reaches HTTPS
They had basically installed a deadbolt on the inside of the house and left the front gate open.
What actually happened
They found the problem during an external review. A tester visited http://app.example.com, intercepted traffic, and saw:
- a login page served over HTTP
- a non-secure analytics script request
- one old cookie missing the
Secureflag
The team’s response was, “But we have HSTS.”
Nope. HSTS is a response header. If the first response is over HTTP, the browser hasn’t learned the HSTS policy yet. It cannot enforce a rule it hasn’t received.
The fixed version
After
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/ssl/app/fullchain.pem;
ssl_certificate_key /etc/ssl/app/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}
That fixed the first-visit problem. HTTP no longer serves content. It only redirects.
Then they tightened cookies too:
Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax
Now the security model made sense:
- HTTP requests get bounced immediately to HTTPS
- HTTPS responses teach the browser to skip HTTP next time
- Secure cookies stop leaking over plaintext connections
That’s the right layering.
Why “HSTS first” is usually the wrong mental model
Developers sometimes ask, “Should I enable HSTS before the redirect?”
Operationally, sure, you can deploy the HSTS header on the HTTPS vhost first. That won’t hurt by itself. But from a user protection standpoint, the redirect is what saves the first request.
HSTS only helps if one of these is true:
- The browser has seen your HSTS header before
- Your domain is in the HSTS preload list
If neither is true, the browser has no reason to auto-upgrade the request before talking to your server.
That’s why I tell teams:
- Redirect protects first contact
- HSTS protects future contact
- Preload protects even first contact, but only after careful rollout
The common rollout trap: includeSubDomains
This is where people break production.
A company wants to “do HSTS properly” and ships:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
That’s aggressive. Sometimes too aggressive.
One team had:
www.example.comon HTTPSapp.example.comon HTTPSold-api.example.comstill on HTTP internallym.example.comabandoned but still resolving
The minute browsers cached includeSubDomains, those subdomains started failing hard for affected users. No click-through. No bypass. That’s exactly how HSTS is supposed to work.
The safer rollout looked like this:
Stage 1: redirect everything possible to HTTPS
<VirtualHost *:80>
ServerName www.example.com
Redirect permanent / https://www.example.com/
</VirtualHost>
And for app traffic:
<VirtualHost *:443>
ServerName www.example.com
Header always set Strict-Transport-Security "max-age=300"
ProxyPass / http://backend/
ProxyPassReverse / http://backend/
</VirtualHost>
Start with a short max-age, like 5 minutes:
Strict-Transport-Security: max-age=300
Then increase it after validation:
Strict-Transport-Security: max-age=86400
Then later:
Strict-Transport-Security: max-age=31536000
Only add includeSubDomains when you know every subdomain is HTTPS-clean.
Before-and-after request flow
Here’s the difference in user experience.
Before: HSTS header exists, but no HTTP redirect
- Browser requests
http://example.com - Server returns a page over HTTP
- User may submit a form over HTTP
- Browser still doesn’t have HSTS unless it later reaches HTTPS
- First-visit traffic is exposed
After: redirect + HSTS
- Browser requests
http://example.com - Server returns
301 Location: https://example.com/... - Browser reconnects over HTTPS
- Server returns page with HSTS header
- Browser remembers HTTPS-only policy for future visits
After preload
- Browser requests
http://example.com - Browser upgrades internally to HTTPS before sending any request
- No plaintext request hits the network
That last state is great, but you earn it by proving your domain is ready.
How I check this in the real world
I test three things:
- Does HTTP serve any content at all?
- Does HTTPS return the HSTS header on every response where it should?
- Are subdomains actually ready before
includeSubDomains?
A quick external scan helps catch obvious mistakes. If you want a fast sanity check, run your site through Headertest, which gives you a free security headers scan and makes missing HSTS or broken redirect behavior pretty obvious.
Then verify manually with curl, because scanners don’t replace eyeballs:
curl -I http://example.com
Expected:
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
Then:
curl -I https://example.com
Expected:
Strict-Transport-Security: max-age=31536000; includeSubDomains
And if you’re behind a CDN or load balancer, check there too. I’ve seen origin configs look perfect while the edge quietly strips headers or mishandles redirects.
My recommendation for production
If you want the practical order:
- Get HTTPS working everywhere
- Redirect all HTTP to HTTPS
- Set HSTS on HTTPS with a low
max-age - Increase
max-ageafter validation - Add
includeSubDomainsonly when every subdomain is ready - Consider preload only when you’re sure you won’t need to back out
That’s the boring answer, which usually means it’s the right one.
Final rule of thumb
If you remember one thing, make it this:
An HTTPS redirect protects the first visit. HSTS protects the next one.
And if your site is preloaded, HSTS can protect the first one too. But don’t treat preload like a checkbox. Treat it like a one-way door with paperwork.
For most teams, the secure baseline is simple:
- HTTP should do nothing except redirect
- HTTPS should always send HSTS
- Cookies should be
Secure - Subdomains should be audited before you go all-in on
includeSubDomains
That combination holds up in production. Anything less usually turns into a postmortem.