If you’ve ever copied this header without thinking too hard about it, you’re not alone:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
It shows up in blog posts, server templates, security scanners, and “best practices” snippets. Then six months later someone realizes staging is broken, a subdomain can’t be reached, or the preload submission was a terrible idea.
The biggest source of confusion is max-age. People treat it like a vague “security on/off” switch. It’s not. It’s a cache lifetime, and getting it wrong can lock users into behavior you didn’t mean to enforce.
What max-age actually does
max-age tells the browser how long, in seconds, it should remember that your site must only be loaded over HTTPS.
Example:
Strict-Transport-Security: max-age=31536000
That means: for the next 31,536,000 seconds (1 year), if the browser sees http://example.com, it should internally rewrite that to https://example.com before making the request.
That matters because HSTS is a client-side memory. Once the browser learns the rule over HTTPS, it keeps enforcing it until the timer expires.
A few practical implications:
- HSTS only works after the browser has received the header over HTTPS.
max-ageis refreshed every time the browser sees the header again.- A long
max-ageis sticky. If you make a mistake, users keep that mistake cached. max-age=0tells browsers to forget the policy, but only after they successfully receive that updated header over HTTPS.
That last point trips people up constantly.
Mistake #1: Starting with a one-year max-age
This is the classic one. A team enables HSTS with a 1-year value because a scanner said so:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Looks fine until someone remembers:
old-api.example.comstill serves plain HTTPdev.example.comhas a broken certm.example.comis handled by a third partycdn.example.comdoesn’t consistently support HTTPS
Now browsers that saw the header will refuse to use HTTP for those hosts if includeSubDomains is present.
Fix
Roll it out in stages.
Start small:
Strict-Transport-Security: max-age=300
That’s 5 minutes. Then move to:
Strict-Transport-Security: max-age=86400
Then maybe a week:
Strict-Transport-Security: max-age=604800
Only after you’re confident every path, redirect, and subdomain is clean should you move to a long-lived value like:
Strict-Transport-Security: max-age=31536000; includeSubDomains
I prefer boring rollouts here. HSTS is one of those controls where “move fast” usually means “break production for cached browsers.”
Mistake #2: Not understanding that includeSubDomains changes the blast radius
Without includeSubDomains, HSTS applies only to the exact host that served the header.
So if www.example.com sends:
Strict-Transport-Security: max-age=31536000
That does not automatically cover api.example.com.
But if you send:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Now every subdomain is expected to support HTTPS properly.
Fix
Inventory your subdomains before adding includeSubDomains.
That means all of them, including weird forgotten ones:
- old admin panels
- vanity redirects
- mail-related hosts exposed on the web
- third-party delegated subdomains
- internal tools accidentally public in DNS
If you’re not sure what’s currently exposed, a quick header scan can help spot what’s actually returning security headers. You can run a free check at HeaderTest.
My rule: if you can’t confidently say every public subdomain has valid HTTPS, don’t turn on includeSubDomains yet.
Mistake #3: Thinking preload is just “extra secure”
Preload is not just another flag to sprinkle in because a template said so.
This header:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
signals that you want your domain considered for browser preload lists. Once a domain is preloaded, browsers ship with HTTPS-only behavior for that domain baked in.
That’s powerful, but it’s also hard to unwind.
Fix
Treat preload like a long-term commitment.
Before you even think about it:
- HTTPS must work everywhere
- all subdomains must be HTTPS-capable
- redirects must be clean
- certificate management must be reliable
- your org must understand what preloading means operationally
If you’re still fixing inconsistent TLS on random subdomains, preload is premature.
For the exact preload requirements and browser behavior, read the official docs:
Mistake #4: Sending HSTS on HTTP responses and expecting it to work
Browsers ignore HSTS headers sent over plain HTTP. That’s by design.
This does nothing useful:
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000
if it was delivered over HTTP.
The browser must receive the header over a valid HTTPS connection.
Fix
Serve HSTS only on HTTPS responses, and make sure your HTTP endpoint redirects immediately to HTTPS.
A sane Nginx setup looks like this:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
add_header Strict-Transport-Security "max-age=86400" always;
location / {
proxy_pass http://app;
}
}
The key bit is that the HSTS header is added on the HTTPS vhost, not the HTTP one.
Mistake #5: Forgetting that errors and redirects need the header too
A lot of teams only add HSTS on successful 200 OK responses. Then a redirect, error page, or cached edge response skips it.
That weakens consistency and slows policy refresh.
Fix
Set the header broadly, including redirects and error responses.
In Nginx, use always:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
In Apache:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
If you’re behind a CDN or load balancer, verify the final response seen by the browser still includes the header. Don’t assume your origin config survives the edge unchanged.
Mistake #6: Using max-age=0 and expecting instant rollback
When HSTS causes trouble, somebody eventually says: “Just set max-age=0.”
That is the correct removal mechanism:
Strict-Transport-Security: max-age=0
But it’s not magic. The browser must first reach your site over HTTPS and receive that header. If the cert is broken, or the subdomain is inaccessible over HTTPS, users may never get the rollback instruction.
Fix
Plan rollback before rollout.
A safe process looks like this:
- Start with a small
max-age - Validate all subdomains
- Increase gradually
- Avoid preload until you’re truly ready
If you need to remove HSTS, send:
Strict-Transport-Security: max-age=0
over valid HTTPS consistently across the affected hostnames. Then wait for browsers to update their cached state.
This is exactly why I don’t like going straight to one year on day one. Recovery gets ugly fast.
Mistake #7: Applying the same max-age everywhere without context
Developers often ask for the “right” HSTS max-age as if there’s one universal answer.
There isn’t. There’s a rollout value and a steady-state value.
Fix
Use a staged policy:
- Testing rollout:
max-age=300 - Early production confidence:
max-age=86400 - Stable deployment:
max-age=604800 - Mature long-term policy:
max-age=31536000
That gives you room to catch surprises before they become cached for months.
If your infrastructure is messy, your max-age should reflect that reality. Security controls don’t get stronger because you declare a bigger number. They get stronger when your environment can actually support the policy.
Mistake #8: Assuming a redirect to HTTPS makes HSTS unnecessary
A redirect is not a replacement for HSTS.
Without HSTS, the first visit to http://example.com can still be intercepted before the browser gets redirected. HSTS closes that gap after the browser has learned the policy. Preload closes it even for first-time visits.
Fix
Use both:
- HTTP to HTTPS redirect
- HSTS on HTTPS responses
That combination is the baseline.
A good default once you’re ready
If your whole domain and all public subdomains are consistently HTTPS-only, this is a reasonable mature policy:
Strict-Transport-Security: max-age=31536000; includeSubDomains
If you’ve fully met preload requirements and understand the operational cost:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
If you’re not fully sure yet, don’t fake confidence with a giant max-age.
Quick checklist
Before increasing HSTS max-age, I’d verify:
- HTTPS works on every public hostname
- certificates renew reliably
- HTTP always redirects to HTTPS
- redirects and error pages include HSTS
includeSubDomainswon’t break forgotten hosts- preload is a deliberate choice, not copied boilerplate
And after deployment, test the actual response your browser receives. Use browser dev tools, curl, or run a scan with HeaderTest.
HSTS is simple once you stop treating max-age like decoration. It’s a browser cache timer with teeth. Set it like you expect to live with the consequences.