HSTS on Cloudflare Pages looks easy right up until it isn’t.

You add a Strict-Transport-Security header, verify it in the browser, and move on. Then a week later you realize preview URLs behave differently, your apex domain redirects through a weird chain, or someone turned on preload without thinking about subdomains that still speak plain HTTP.

I’ve seen this pattern a lot: HSTS gets treated like a checkbox. It’s not. On Cloudflare Pages, it’s simple to enable, but easy to misconfigure in ways that are annoying at best and production-breaking at worst.

Here are the mistakes I see most often, and how I’d fix them.

Mistake #1: Sending HSTS on a domain that still has HTTP somewhere

HSTS tells browsers: “for this host, only use HTTPS from now on.” That’s great if your domain is fully HTTPS and stays that way. It’s a mess if any part of your setup still expects HTTP.

A common example:

  • example.com redirects to www.example.com
  • www.example.com is HTTPS-only
  • but some old subdomain like old.example.com still serves over HTTP

If you set:

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

you just told browsers to force HTTPS on old.example.com too.

If that host doesn’t support HTTPS, users get hard failures. No click-through warning. No fallback. Just broken.

Fix

Before enabling HSTS, especially with includeSubDomains, inventory your actual hosts:

  • apex domain
  • www
  • app subdomains
  • staging subdomains
  • legacy services
  • mail or non-web hosts that may be exposed in weird ways

If every web-facing subdomain supports HTTPS, good. If not, either:

  1. fix those subdomains first, or
  2. avoid includeSubDomains for now

A safer starting point is:

Strict-Transport-Security: max-age=300

That gives you a 5-minute test window. If everything behaves correctly, increase it gradually.

Mistake #2: Starting with a one-year max-age on day one

This is the classic “looks secure, feels secure, bites later” move.

People copy:

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

straight into production without testing.

That header tells browsers to remember the HTTPS-only policy for a year. If you made a mistake, you can change the header later, but browsers that already cached it won’t forget until the max-age expires, unless you explicitly clear it with a new header and users revisit over HTTPS.

That’s not a fun rollback.

Fix

Ramp HSTS up in stages.

I’d usually do this:

Strict-Transport-Security: max-age=300

Then:

Strict-Transport-Security: max-age=86400

Then:

Strict-Transport-Security: max-age=31536000

Only after that would I consider:

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

And only much later:

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

Slow is better here. HSTS is one of those headers where caution is a feature, not a sign of weakness.

Mistake #3: Enabling preload because it sounds more secure

preload has real value. It can protect the very first request before the browser has ever seen your HSTS header. But people treat it like a badge instead of a commitment.

Once your domain is preloaded by browsers, backing out is slow and painful. You’re no longer just relying on cached browser state. You’re in browser-maintained preload lists.

On Cloudflare Pages, this gets risky when teams have a clean main site but a messy domain setup around it.

Fix

Don’t use preload unless all of these are true:

  • your apex domain supports HTTPS
  • your www host supports HTTPS
  • all subdomains you care about support HTTPS
  • you want includeSubDomains
  • you can commit to HTTPS long-term everywhere

If you’re not fully confident, skip it.

A strong HSTS policy without preload is still very good:

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

That covers a lot of real-world risk without locking you into preload requirements too early.

Mistake #4: Setting HSTS on preview deployments and assuming production matches

Cloudflare Pages gives you preview deployments on *.pages.dev or custom preview environments depending on your setup. That’s convenient, but it can hide problems.

You might test HSTS on a preview URL and think everything is fine, while your production custom domain has different redirects, DNS behavior, or edge settings.

Worse, you might accidentally apply a policy to an environment that shouldn’t inherit production assumptions.

Fix

Test the actual production hostnames that users visit.

That means checking:

  • https://example.com
  • https://www.example.com
  • any custom app hostnames
  • any redirecting variants

Don’t stop at “the site loads over HTTPS.” Inspect the final response headers and redirect chain.

For a quick check, run your domain through HeaderTest. It makes it easy to spot whether HSTS is present on the final HTTPS response and whether the redirect path is clean.

Mistake #5: Adding HSTS in the wrong place

With Cloudflare Pages, developers often assume any header config will apply everywhere automatically. Not always.

Depending on your setup, headers might be defined in:

  • a _headers file in your Pages project
  • framework config
  • Cloudflare-managed rules or transforms
  • origin logic if you’re mixing Pages with functions or other services

That can lead to duplicate headers, missing headers on some routes, or headers applied only to static assets but not HTML responses.

Fix

For a Pages project, I prefer a _headers file when possible because it’s explicit and lives with the app.

Example:

/*
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin

If you’re serving mixed routes or using Pages Functions, verify that dynamic responses also include the header.

A simple Pages Function example:

export async function onRequest(context) {
  const response = await context.next();
  const newResponse = new Response(response.body, response);

  newResponse.headers.set(
    "Strict-Transport-Security",
    "max-age=31536000; includeSubDomains"
  );

  return newResponse;
}

Pick one source of truth if you can. Debugging layered header config is annoying and rarely worth the cleverness.

Mistake #6: Forgetting that HSTS only works over HTTPS

Browsers ignore HSTS headers sent over plain HTTP. That’s by design, because an attacker on an insecure connection could inject fake policies.

I still see people test http://example.com, inspect the redirect response, find no HSTS header, and assume something is broken.

That’s not the right test.

Fix

Check the final HTTPS response:

curl -I https://example.com

You want to see something like:

HTTP/2 200
strict-transport-security: max-age=31536000; includeSubDomains

It’s also worth checking redirects:

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

But the HSTS policy itself must be delivered over HTTPS to matter.

Mistake #7: Using includeSubDomains without thinking about non-Pages services

Cloudflare Pages may host your front end, but your domain probably includes other stuff:

  • API on api.example.com
  • docs on docs.example.com
  • admin on admin.example.com
  • old marketing microsite on promo.example.com

If the front end team adds includeSubDomains on the main site without coordinating, they can accidentally force HTTPS for systems they don’t own.

This is less a technical bug and more an org chart bug.

Fix

Treat includeSubDomains as a domain-wide change, not a Pages-only setting.

Ask:

  • Who owns our subdomains?
  • Which ones are public?
  • Which ones terminate TLS correctly?
  • Which ones are temporary, legacy, or forgotten?

If you can’t answer that, don’t enable includeSubDomains yet.

Mistake #8: Not having a rollback plan

HSTS mistakes are recoverable, but not instantly. That’s why I like having the rollback already decided before I raise max-age.

If something goes wrong, you need to know the exact header you’ll deploy to reduce or clear the policy.

Fix

Your rollback header is:

Strict-Transport-Security: max-age=0

That tells browsers to forget the cached HSTS policy when they next receive it over HTTPS.

If you previously used includeSubDomains or preload, rollback gets more complicated operationally, especially with preload. Another reason not to jump there too fast.

A practical Cloudflare Pages setup

If your domain is fully HTTPS and you want a sane default, this is a good production policy:

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

Put that in _headers.

If you’re early in rollout, use this first:

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

Then verify:

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

And scan the live site with HeaderTest to catch missing coverage or conflicting headers.

What I’d actually do

For most Cloudflare Pages projects, I’d ship HSTS in this order:

  1. enable HTTPS everywhere
  2. clean up redirects
  3. set max-age=300
  4. verify production hosts
  5. raise to 86400
  6. raise to 31536000
  7. add includeSubDomains only after checking the whole domain
  8. ignore preload unless I’m very sure I want that commitment

That’s boring, which is exactly why it works.

HSTS is one of the best low-effort web security controls you can deploy on Cloudflare Pages. The trick is not adding it. The trick is adding it without surprising yourself six months later.