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-age is refreshed every time the browser sees the header again.
  • A long max-age is sticky. If you make a mistake, users keep that mistake cached.
  • max-age=0 tells 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.com still serves plain HTTP
  • dev.example.com has a broken cert
  • m.example.com is handled by a third party
  • cdn.example.com doesn’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:

  1. Start with a small max-age
  2. Validate all subdomains
  3. Increase gradually
  4. 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
  • includeSubDomains won’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.