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.comredirects towww.example.comwww.example.comis HTTPS-only- but some old subdomain like
old.example.comstill 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:
- fix those subdomains first, or
- avoid
includeSubDomainsfor 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
wwwhost 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.comhttps://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
_headersfile 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:
- enable HTTPS everywhere
- clean up redirects
- set
max-age=300 - verify production hosts
- raise to
86400 - raise to
31536000 - add
includeSubDomainsonly after checking the whole domain - ignore
preloadunless 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.