HSTS on Vercel is one of those settings that looks trivial right up until you lock yourself out of a subdomain for a year.

If you deploy on Vercel, you already get HTTPS by default. That solves transport encryption. HSTS solves a different problem: making browsers refuse plain HTTP for your domain after they’ve seen your policy once.

That sounds great, and usually it is. But HSTS is also sticky, cached aggressively by browsers, and very easy to over-apply. I’ve seen teams flip on includeSubDomains without thinking through preview apps, legacy subdomains, or weird internal tools hanging off the same parent domain.

So here’s the real comparison guide: when HSTS on Vercel is worth it, when it’s risky, and how to set it up without creating your own outage.

What HSTS actually does

HSTS stands for HTTP Strict Transport Security. It’s sent as a response header:

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

Once a browser sees that over HTTPS, it remembers:

  • always use HTTPS for this host
  • never allow the user to click through certificate errors for this host
  • optionally apply the rule to all subdomains if includeSubDomains is present

That means HSTS helps block:

  • downgrade attempts from HTTPS to HTTP
  • accidental insecure links like http://example.com
  • some SSL stripping attacks on first-party revisits

What it does not do:

  • magically secure your app logic
  • fix mixed content you already ship
  • protect the very first visit unless you use preload
  • replace redirects, CSP, or sane cookie settings

Why HSTS fits Vercel well

Vercel is actually a pretty good environment for HSTS because the platform already enforces a lot of the plumbing you want:

  • automatic TLS
  • managed certificates
  • straightforward custom domain support
  • easy header configuration through app config

That lowers the operational cost of HSTS. On an old self-managed stack, I’d worry more about cert renewal drift and random edge cases. On Vercel, the baseline is cleaner.

For most production sites on a stable domain, HSTS is a solid default.

The main options on Vercel

You generally have a few ways to send HSTS on Vercel deployments:

  1. set headers in vercel.json
  2. set headers in next.config.js or next.config.mjs for Next.js
  3. inject headers in middleware or application code

My strong preference: configure it statically when possible.

Static config is easier to audit, easier to reason about, and less likely to disappear during a refactor.

Option 1: vercel.json

This works well for many Vercel apps:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Strict-Transport-Security",
          "value": "max-age=31536000; includeSubDomains"
        }
      ]
    }
  ]
}

Option 2: Next.js headers()

If you’re running Next.js, this is usually the cleanest app-level option:

/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Strict-Transport-Security',
            value: 'max-age=31536000; includeSubDomains',
          },
        ],
      },
    ]
  },
}

export default nextConfig

Official docs for Next.js headers are here: https://nextjs.org/docs/app/api-reference/config/next-config-js/headers

For Vercel project configuration, see: https://vercel.com/docs/project-configuration

Pros of enabling HSTS on Vercel

1. It closes the HTTP back door

Even if you redirect HTTP to HTTPS, there’s still a moment where the browser may attempt plain HTTP first. HSTS removes that on repeat visits.

For user-facing production domains, that’s exactly what you want.

If your site depends on secure cookies, OAuth callbacks, login state, or account pages, forcing the browser to stay on HTTPS reduces accidental insecure requests.

It’s not a replacement for Secure cookies, but it complements them well.

3. It’s low-maintenance on a managed platform

This is the big Vercel-specific advantage. Since Vercel handles certificates and HTTPS delivery cleanly, HSTS is less scary than it is on hand-rolled infrastructure.

You still need to think, but you don’t need to babysit the transport layer.

4. It helps enforce good domain hygiene

Once you commit to HSTS, you stop treating HTTP as a fallback. That pressure is healthy. It forces teams to clean up old links, old redirects, and weird assumptions.

I like that. Security headers should push architecture in the right direction.

Cons of enabling HSTS on Vercel

1. Browser caching makes mistakes painful

This is the big one.

If you ship:

Strict-Transport-Security: max-age=31536000

you’ve told browsers to remember that policy for a year.

If you break TLS, move traffic badly, or realize you shouldn’t have applied HSTS to that domain, users may keep getting forced to HTTPS until the cache expires.

This is why I never start with a 1-year policy on a fresh domain setup.

2. includeSubDomains can break things you forgot existed

This is where teams get burned.

If your main site is example.com and you send:

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

then browsers will enforce HTTPS for:

  • www.example.com
  • api.example.com
  • old.example.com
  • status.example.com
  • every forgotten subdomain someone created three years ago

On Vercel, your production app might be fine while some unrelated subdomain outside Vercel is not. HSTS doesn’t care who owns the infrastructure. It applies to the whole domain tree.

3. Preview and non-production hostname strategy matters

Vercel preview URLs use Vercel-owned domains, so your own apex HSTS policy won’t automatically spill into those. That’s good.

But if your team uses custom preview subdomains like:

  • staging.example.com
  • qa.example.com
  • feature-x.example.com

then includeSubDomains absolutely affects them.

If those environments aren’t consistently HTTPS-clean, don’t enable broad HSTS yet.

4. Preload is even less forgiving

If you go beyond normal HSTS and aim for preload, you’re asking browsers to hardcode HTTPS for your domain family.

That’s powerful and, frankly, easy to romanticize. But preload is not where you start. Preload is where you end up after you’ve proven your domain setup is boring and stable.

For preload, the header usually looks like:

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

I would not recommend this for most teams until they’ve lived happily with standard HSTS for a while.

Best rollout strategy for Vercel

This is the part that saves you from regret.

Phase 1: start small

Ship this first:

Strict-Transport-Security: max-age=300

Five minutes. That’s it.

Confirm:

  • the header appears on HTTPS responses
  • HTTP redirects still behave as expected
  • no broken subdomain assumptions exist
  • certs are healthy everywhere you care about

You can verify your headers with browser devtools or run a quick scan at https://headertest.com.

Phase 2: increase duration

Then move to something like:

Strict-Transport-Security: max-age=86400

Then:

Strict-Transport-Security: max-age=2592000

Then eventually:

Strict-Transport-Security: max-age=31536000

I like gradual rollouts because browser cache behavior is unforgiving. Being conservative here is not cowardice; it’s competence.

Phase 3: decide on includeSubDomains

Only add it if you can answer yes to this:

  • Are all current subdomains HTTPS-only and certificate-valid?
  • Are future subdomains controlled enough that this won’t become a surprise?
  • Are there no legacy tools, old mail panels, forgotten DNS entries, or vendor-hosted subdomains hanging around?

If the answer is “probably,” that means “no.”

Safer baseline for a production domain

const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Strict-Transport-Security',
            value: 'max-age=31536000',
          },
        ],
      },
    ]
  },
}

export default nextConfig

Stronger policy for a well-managed domain family

const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Strict-Transport-Security',
            value: 'max-age=31536000; includeSubDomains',
          },
        ],
      },
    ]
  },
}

export default nextConfig

Preload-ready policy only for mature setups

const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Strict-Transport-Security',
            value: 'max-age=63072000; includeSubDomains; preload',
          },
        ],
      },
    ]
  },
}

export default nextConfig

If you’re considering preload, read the official HSTS reference first: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security

My opinionated take

For Vercel production deployments, HSTS is usually a yes.

For includeSubDomains, it’s a “slow down and prove it.”

For preload, it’s a “not unless your domain operations are boring enough that nobody has to think about them.”

That’s the real comparison:

  • HSTS without subdomains: strong default, low drama
  • HSTS with includeSubDomains: stronger, but requires domain discipline
  • HSTS preload: strongest, highest commitment, least reversible

If your site is a normal production app on Vercel, I’d enable HSTS. I’d start with a short max-age, increase it gradually, and only expand scope once I knew every subdomain was under control.

That approach is less flashy than dropping a massive preload header on day one, but it’s the one that avoids emergency Slack threads later.