HSTS preloading is one of those rare web security features that’s both boring and incredibly useful. If you run a real production site, especially one that handles logins, payments, admin panels, or anything remotely sensitive, getting onto the HSTS preload list is usually worth doing.

Why? Because normal HSTS only starts protecting users after they’ve visited your site once over HTTPS and received the Strict-Transport-Security header. Preloading removes that first-visit gap. Browsers ship with your domain baked into a hardcoded HTTPS-only list, so they’ll never attempt plain HTTP in the first place.

That’s the good news. The catch is that preloading is intentionally strict. You need to configure HSTS correctly, commit to HTTPS across your whole domain tree, and understand that rolling it back can be slow and annoying.

This tutorial walks through what the preload list is, the exact requirements, how to configure headers, how to submit your domain, and what can go wrong.

What the HSTS preload list actually does

Normally, HSTS works like this:

  1. User visits https://example.com
  2. Your server sends:
Strict-Transport-Security: max-age=31536000; includeSubDomains
  1. The browser remembers that example.com must use HTTPS for the next year

That’s fine, but it doesn’t protect the very first visit. A user typing example.com might still start with http://example.com, and a network attacker could potentially interfere before the redirect to HTTPS happens.

The preload list fixes that by letting browser vendors ship domains that should always use HTTPS from the start. If your domain is preloaded, browsers know before making any request that your site must be contacted only over HTTPS.

In practice, this means:

  • http://example.com is upgraded internally to HTTPS by the browser
  • certificate errors can’t be bypassed the usual way
  • subdomains are also forced to HTTPS if you preload correctly

It’s a strong commitment. That last point matters a lot more than people think.

Before you preload: understand the commitment

Preloading is not just “turn on a stronger header.” It’s more like saying:

  • my apex domain supports HTTPS forever
  • all current subdomains support HTTPS
  • all future subdomains will support HTTPS too
  • I will not need a plain HTTP-only host anywhere under this domain

That includes weird legacy names like:

  • old-admin.example.com
  • mta-sts.example.com
  • helpdesk.example.com
  • dev.example.com
  • printer.example.com

If any subdomain is publicly reachable and doesn’t support HTTPS properly, preloading can break it for users in major browsers.

So before doing anything else, inventory your subdomains. Seriously. This is where most bad preload attempts go wrong.

The preload requirements

To get on the preload list, your domain generally needs to meet these requirements:

  • serve a valid certificate
  • redirect all HTTP traffic to HTTPS
  • serve HSTS on the HTTPS response
  • use max-age of at least 31536000
  • include includeSubDomains
  • include the preload token
  • apply the policy on the apex domain, usually example.com
  • apply the policy on www.example.com too if it exists

A valid preload header looks like this:

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

That exact shape matters. If you leave out includeSubDomains or use a shorter max-age, your submission will fail.

Step 1: make sure HTTP always redirects to HTTPS

Before touching HSTS, make sure every HTTP request gets a clean redirect to HTTPS.

Nginx example

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    return 301 https://$host$request_uri;
}

Apache example

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    Redirect permanent / https://example.com/
</VirtualHost>

If you want to preserve the host and path more explicitly in Apache:

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com

    RewriteEngine On
    RewriteCond %{HTTPS} !=on
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>

Caddy example

example.com, www.example.com {
    redir https://{host}{uri} permanent
}

This redirect should happen for every path, not just the homepage.

Step 2: serve the HSTS header on HTTPS

Do not send HSTS over HTTP. Browsers ignore it there anyway. Send it only on HTTPS responses.

Nginx example

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate /etc/ssl/example/fullchain.pem;
    ssl_certificate_key /etc/ssl/example/privkey.pem;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    root /var/www/html;
}

That always flag is important in Nginx because it ensures the header is added even on error responses.

Apache example

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com

    SSLEngine on
    SSLCertificateFile /etc/ssl/example/fullchain.pem
    SSLCertificateKeyFile /etc/ssl/example/privkey.pem

    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</VirtualHost>

You’ll need mod_headers enabled for that.

Express / Node.js example

If you terminate TLS in Node directly:

app.use((req, res, next) => {
  if (req.secure) {
    res.setHeader(
      'Strict-Transport-Security',
      'max-age=31536000; includeSubDomains; preload'
    );
  }
  next();
});

If you’re behind a reverse proxy, make sure req.secure is trustworthy:

app.set('trust proxy', true);

app.use((req, res, next) => {
  if (req.secure) {
    res.setHeader(
      'Strict-Transport-Security',
      'max-age=31536000; includeSubDomains; preload'
    );
  }
  next();
});

In many setups, it’s cleaner to set HSTS at the reverse proxy or load balancer instead of inside the app.

Step 3: start safely before going full preload

Here’s my opinionated advice: don’t jump straight to preload in production unless you’re absolutely sure your domain is clean.

A safer rollout looks like this:

Phase 1: short max-age

Strict-Transport-Security: max-age=300

That’s five minutes. Verify nothing weird breaks.

Phase 2: longer max-age without preload

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

That’s one day. Then maybe move to a week:

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

Then to a year:

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

Phase 3: add preload token

Once you’re confident all subdomains are HTTPS-safe:

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

Yes, preloading requires one year. No, there’s no practical shortcut around that.

Step 4: verify your domain and subdomains

At minimum, check:

  • https://example.com
  • https://www.example.com
  • http://example.com
  • http://www.example.com

You should also test any known subdomains that users might hit directly.

Use curl to inspect the response headers:

curl -I http://example.com

Expected result: a redirect to HTTPS.

HTTP/1.1 301 Moved Permanently
Location: https://example.com/

Then check HTTPS:

curl -I https://example.com

Expected result includes:

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

Also test www:

curl -I https://www.example.com

And if you redirect www to apex or the other way around, make sure the HSTS header is still present on the HTTPS response involved in that path.

Test your HSTS configuration and other security headers at headertest.com - free, instant, no signup required.

Step 5: submit to the preload list

The canonical submission point is the HSTS preload service used by browsers. You submit your domain, it runs checks, and if everything passes, your domain becomes eligible for inclusion in the generated preload list.

The practical flow is:

  1. set the required header on your production site
  2. confirm redirects and certificate validity
  3. confirm includeSubDomains won’t break anything
  4. submit the apex domain
  5. wait for browser release cycles

This last part surprises people: passing submission checks does not mean every user is instantly protected. It still takes time for browser vendors to ship updated preload data.

So don’t think of preload as a same-day switch. Think of it as a permanent hardening step that gradually propagates.

Common mistakes that block preload

Missing includeSubDomains

This is the classic one. Without it, preload won’t happen.

Bad:

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

Good:

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

Max-age too short

Also common.

Bad:

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

Good:

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

Redirecting to HTTPS but not sending HSTS on the HTTPS site

A redirect alone is not enough. You need the actual HSTS header on the final HTTPS response.

Broken subdomains

This is the preload killer that shows up after submission. Maybe blog.example.com is fine, but legacy.example.com has an expired cert, or dev.example.com is public and only works over HTTP. With includeSubDomains, that’s now your problem.

CDN or proxy stripping the header

Sometimes your origin sends HSTS correctly, but the CDN normalizes or removes it. Always test the public edge response, not just the backend.

How to check subdomains before preloading

You don’t need a perfect map of every internal hostname, but you do need confidence that anything publicly resolvable under your domain won’t become unusable under HTTPS-only rules.

A practical approach:

Query DNS records

List known hosts from your DNS provider and zone files.

Look for:

  • A
  • AAAA
  • CNAME

Crawl certificates

Certificate transparency logs can expose subdomains that your org has used publicly.

Test known hosts with curl

curl -I http://sub.example.com
curl -I https://sub.example.com

You want one of these outcomes:

  • HTTPS works with a valid cert
  • the hostname doesn’t publicly resolve
  • the hostname is intentionally retired and inaccessible

What you do not want is a live HTTP-only service.

How to remove a domain from the preload list

You can request removal, but this is not a quick undo button. Removal takes time, browser updates take time, and users may stay on old versions for a while.

That means if preloading breaks something critical, the blast radius can last weeks or longer.

This is why I’m pretty conservative about recommending preload for organizations with messy DNS, random inherited subdomains, or teams that love spinning up public test environments under the main production domain.

If that sounds like your company, fix your naming and subdomain hygiene first.

Best practices if you plan to preload

Use a separate domain for experiments

Don’t put half-broken dev systems under your main preloaded domain if you can avoid it.

For example, instead of:

  • dev.example.com
  • qa.example.com

consider using a separate non-production domain entirely.

Automate certificate management

If preloading means every subdomain must support HTTPS, manual certificate processes become a liability. Use ACME automation where possible.

Standardize redirects and headers at the edge

Set HTTPS redirects and HSTS in one place:

  • load balancer
  • ingress controller
  • CDN
  • reverse proxy

That reduces drift between apps.

Document the preload commitment

Future teams will forget. Leave a note in:

  • infra repos
  • runbooks
  • DNS management docs
  • platform standards

Make it explicit that new public subdomains must support HTTPS from day one.

A simple production-ready example

Here’s a common Nginx setup for apex plus www, with HTTP redirected and HSTS enabled:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    root /var/www/example;
    index index.html;
}

This is boring, and that’s exactly what you want. Security headers should be boring.

Final advice

If your site is stable, fully HTTPS, and you control your subdomains responsibly, get on the preload list. It closes a real first-visit security gap and strengthens your HTTPS posture in a way users never have to think about.

But don’t treat it like a checkbox. Preloading is a long-term domain policy, not a tweak.

My practical recommendation is:

  1. enable HTTPS everywhere
  2. roll out HSTS gradually
  3. verify all public subdomains
  4. add includeSubDomains
  5. add preload
  6. submit only when you’re confident you won’t regret it

That last step matters most. HSTS preload is excellent when you’re ready for it, and deeply annoying when you aren’t.