HTTP Strict Transport Security, or HSTS, is one of those headers that looks trivial until you ship the wrong value and lock yourself into a bad decision for months.

If you deploy on Netlify, adding HSTS is easy. Adding it safely takes a bit more thought.

This guide covers the practical side: what HSTS actually does, how to configure it on Netlify, how to avoid common mistakes, and how I’d roll it out on a real production site.

What HSTS does

HSTS tells browsers:

  • only use HTTPS for this site
  • automatically rewrite future HTTP requests to HTTPS
  • optionally apply the rule to all subdomains
  • optionally qualify the domain for browser preload lists

A typical header looks like this:

Strict-Transport-Security: max-age=31536000; includeSubDomains

That means:

  • max-age=31536000: remember this rule for 1 year
  • includeSubDomains: apply it to all subdomains too

Once a browser sees that header over a valid HTTPS connection, it will stop trying plain HTTP for that host during the policy lifetime.

That matters because a plain HTTP first request is where downgrade attacks and SSL stripping happen. HSTS closes that gap after the first secure visit.

What HSTS does not do

HSTS does not replace:

  • valid TLS certificates
  • redirecting HTTP to HTTPS
  • secure cookie settings
  • CSP, X-Frame-Options, or other security headers

It also does not help if a user has never visited your site before, unless your domain is on the browser preload list.

Why HSTS on Netlify is straightforward

Netlify already gives you HTTPS and can force redirects to HTTPS. HSTS is just another response header, so you can configure it through:

  • a _headers file
  • netlify.toml

I prefer netlify.toml for maintainability if the project already uses it. If the site is static and header rules are simple, _headers is perfectly fine.

Before you enable HSTS

Do these checks first.

1. Make sure HTTPS works everywhere

Every hostname you care about must serve valid HTTPS:

  • apex domain, like example.com
  • www subdomain, if used
  • any other subdomains if you plan to use includeSubDomains

If you set includeSubDomains and one of your subdomains still serves over HTTP only, you just broke that subdomain for HSTS-capable browsers.

2. Redirect HTTP to HTTPS

Netlify can enforce HTTPS, and you should absolutely do that before HSTS. HSTS tells browsers what to do on future requests. Redirects still matter for first-time visitors and non-HSTS clients.

3. Know your subdomain inventory

This is the part teams skip.

If your company has old subdomains like:

  • status.example.com
  • docs.example.com
  • old-api.example.com
  • dev.example.com

and they aren’t all HTTPS-ready, don’t use includeSubDomains yet.

4. Start with a short max-age

I never start at one year. I start small, verify behavior, then increase it.

A sane rollout looks like:

  • 5 minutes
  • 1 day
  • 1 week
  • 1 year

That gives you room to back out.

Option 1: Configure HSTS with netlify.toml

Here’s a practical Netlify config:

[[headers]]
  for = "/*"
  [headers.values]
    Strict-Transport-Security = "max-age=300"

That sends HSTS for all routes with a 5-minute policy.

After you verify everything works, increase it:

[[headers]]
  for = "/*"
  [headers.values]
    Strict-Transport-Security = "max-age=86400"

Then later:

[[headers]]
  for = "/*"
  [headers.values]
    Strict-Transport-Security = "max-age=31536000"

If you’re ready to include subdomains:

[[headers]]
  for = "/*"
  [headers.values]
    Strict-Transport-Security = "max-age=31536000; includeSubDomains"

If you’re preparing for preload:

[[headers]]
  for = "/*"
  [headers.values]
    Strict-Transport-Security = "max-age=31536000; includeSubDomains; preload"

Be careful with that last one. preload is not just a suggestion. It means you intend to meet preload requirements consistently.

Option 2: Configure HSTS with a _headers file

For static sites, put a _headers file in your publish directory or source directory depending on your build setup.

Example:

/*
  Strict-Transport-Security: max-age=300

Later, increase it:

/*
  Strict-Transport-Security: max-age=31536000; includeSubDomains

Same rules, same risks.

A realistic Netlify setup

Here’s a netlify.toml example I’d actually ship for a production site, including redirects and a few core security headers:

[[redirects]]
  from = "http://example.com/*"
  to = "https://example.com/:splat"
  status = 301
  force = true

[[redirects]]
  from = "http://www.example.com/*"
  to = "https://www.example.com/:splat"
  status = 301
  force = true

[[headers]]
  for = "/*"
  [headers.values]
    Strict-Transport-Security = "max-age=86400"
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"

After a few days without issues, I’d bump HSTS to a longer lifetime.

How to verify HSTS is being sent

You don’t want to guess here. Check the actual response headers.

Using curl

curl -I https://example.com

You want to see something like:

HTTP/2 200
strict-transport-security: max-age=86400
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin

Also test redirects from HTTP:

curl -I http://example.com

You should see a redirect to HTTPS.

Using a browser

Open dev tools, load the page over HTTPS, and inspect the response headers in the network tab.

Using a scanner

If you want a quick external check, run your domain through Headertest. It’s useful for spotting missing or malformed security headers fast.

Common HSTS mistakes on Netlify

These are the ones I see most often.

Setting a huge max-age too early

If you start with:

Strict-Transport-Security: max-age=31536000; includeSubDomains

on day one, you’ve committed browsers for a year. If you missed a subdomain or certificate issue, users can get stuck.

Start small.

Using includeSubDomains without auditing subdomains

This one breaks staging, old apps, forgotten services, and weird internal tools exposed on public DNS.

If you don’t fully control every subdomain, don’t set it yet.

Adding preload because it sounds better

A lot of people treat preload like a security badge. It’s really a long-term operational commitment.

Preloading means browsers hardcode your domain as HTTPS-only before the first visit. That’s great when you’re ready. It’s painful if you aren’t.

I’d only add:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

when all of these are true:

  • your whole domain is HTTPS-only
  • every subdomain is HTTPS-ready
  • HTTP redirects cleanly to HTTPS
  • you’re comfortable keeping that policy long-term

Sending HSTS on HTTP instead of HTTPS

Browsers only honor HSTS when received over a valid HTTPS connection. If you’re testing over HTTP and don’t see it applied, that’s expected.

Forgetting custom domains

Netlify preview URLs and your production custom domain are not the same thing operationally.

What matters most is the domain users actually visit. Verify HSTS there, not just on a generated Netlify subdomain.

Safe rollout plan

If I were enabling HSTS on a Netlify production site today, I’d do this:

Phase 1: enable HTTPS redirects

Make sure all HTTP traffic gets redirected to HTTPS.

Phase 2: set a short HSTS lifetime

[[headers]]
  for = "/*"
  [headers.values]
    Strict-Transport-Security = "max-age=300"

Watch for certificate issues, mixed environment weirdness, and forgotten hostnames.

Phase 3: increase gradually

Move to:

Strict-Transport-Security = "max-age=86400"

Then:

Strict-Transport-Security = "max-age=604800"

Then:

Strict-Transport-Security = "max-age=31536000"

Phase 4: decide on includeSubDomains

Only after you’ve audited subdomains.

Phase 5: decide on preload

Only if you want the operational commitment and meet the requirements.

How to disable HSTS if you need to back out

If you catch a problem early, send:

Strict-Transport-Security: max-age=0

Netlify example:

[[headers]]
  for = "/*"
  [headers.values]
    Strict-Transport-Security = "max-age=0"

That tells browsers to forget the policy.

But there’s a catch: browsers have to successfully reach your site over HTTPS and receive that new header first. If users are already stuck and your HTTPS setup is broken, you can’t rely on this as an emergency escape hatch.

That’s why short initial max-age values are the smart move.

For a stable production site on Netlify that fully supports HTTPS, this is the standard target:

[[headers]]
  for = "/*"
  [headers.values]
    Strict-Transport-Security = "max-age=31536000; includeSubDomains"

If you haven’t audited subdomains, use this instead:

[[headers]]
  for = "/*"
  [headers.values]
    Strict-Transport-Security = "max-age=31536000"

That still gives strong protection for the main host without risking subdomain breakage.

Netlify-specific docs

For the platform details on custom headers and configuration formats, check the official Netlify documentation:

HSTS is easy to add on Netlify. The hard part is resisting the urge to copy a one-year preload policy without thinking through your domain setup first.

My advice: ship redirects, start with a tiny max-age, verify with curl, scan with Headertest, then ramp up deliberately. That’s boring, and boring is exactly what you want from transport security.