HSTS in FastAPI is one of those things that looks trivial until you ship it wrong.

The header itself is simple:

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

But the decision around where to set it, when to enable it, and whether to preload it can absolutely break local development, staging subdomains, and legacy services you forgot still exist.

If you run FastAPI in production, you want HSTS. The real question is which implementation path fits your stack.

What HSTS actually does

HSTS tells browsers: “for this domain, only use HTTPS for a period of time.”

That gives you a few wins:

  • blocks protocol downgrade attacks
  • reduces SSL stripping risk
  • helps prevent users from accidentally using http://
  • enforces HTTPS after the first secure visit

The catch is the “after the first secure visit” part. HSTS is not magic on the first request unless you use preload. I’ll get to that.

For FastAPI apps, you’ll usually choose one of these approaches:

  1. set HSTS in FastAPI middleware
  2. set HSTS at the reverse proxy or load balancer
  3. use a platform or CDN to inject the header
  4. enable preload, or don’t

Option 1: Set HSTS in FastAPI middleware

This is the most obvious Python-native solution. You add middleware and attach the header to every HTTPS response.

Example: custom middleware

from fastapi import FastAPI, Request

app = FastAPI()

HSTS_VALUE = "max-age=31536000; includeSubDomains"

@app.middleware("http")
async def add_security_headers(request: Request, call_next):
    response = await call_next(request)

    # Only send HSTS when the original request was HTTPS
    forwarded_proto = request.headers.get("x-forwarded-proto")
    is_https = request.url.scheme == "https" or forwarded_proto == "https"

    if is_https:
        response.headers["Strict-Transport-Security"] = HSTS_VALUE

    return response

Pros

  • easy to understand in app code
  • versioned with your FastAPI project
  • works well if you own the whole deployment path
  • convenient for app-specific logic

Cons

  • easy to get wrong behind proxies
  • your app may think traffic is HTTP unless proxy headers are trusted correctly
  • duplicated effort if multiple services need the same policy
  • not ideal if TLS terminates before the app

This approach is fine, but I don’t love it as the primary production strategy unless the app really is the right place to manage headers. In most real deployments, TLS terminates at Nginx, Traefik, Envoy, ALB, Cloudflare, or something similar. That layer usually has better context.

FastAPI-specific gotcha

If FastAPI sits behind a reverse proxy, request.url.scheme may be wrong unless forwarded headers are configured properly. If you rely on X-Forwarded-Proto, make sure you trust only your own proxy chain.

If you use Uvicorn directly, review its proxy header handling in the official docs:

And for FastAPI deployment patterns:

Option 2: Set HSTS at the reverse proxy

This is usually my preferred option.

If Nginx or another proxy is terminating TLS, that’s the cleanest place to enforce HSTS. The proxy already knows whether the request came over HTTPS, and it can apply the same policy to multiple apps consistently.

Nginx example

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/ssl/cert.pem;
    ssl_certificate_key /etc/ssl/key.pem;

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

    location / {
        proxy_pass http://fastapi_app;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Pros

  • best place when TLS terminates at the proxy
  • centralized policy for many apps
  • less app code
  • lower chance of scheme detection mistakes
  • easier to standardize across teams

Cons

  • header config lives outside the Python app
  • local parity can be weaker if developers run Uvicorn directly
  • misconfigured proxies can still cause inconsistent behavior
  • changes may require infra deploys rather than app deploys

For most production teams, this is the sweet spot. If you’re serving FastAPI through Nginx, Traefik, HAProxy, Envoy, or a cloud load balancer, put HSTS there unless you have a strong reason not to.

Option 3: Set HSTS at the CDN or edge platform

If your traffic goes through a CDN or edge gateway, you can often inject HSTS there.

Pros

  • simplest central control point
  • consistent across many origins
  • no app or proxy changes required
  • useful for teams with mixed stacks, not just Python

Cons

  • hidden from app developers
  • easy to forget during debugging
  • can conflict with origin behavior
  • edge config drift is a real thing

I’ve seen teams spend too long debugging “why is this header still present?” because the CDN was injecting it even after the app config changed. It works, but it can become a visibility problem.

If you choose this route, document it clearly.

Comparison: app vs proxy vs edge

Here’s the practical version.

FastAPI middleware

Best when:

  • you want app-owned security behavior
  • you don’t have a smart proxy layer
  • each app needs different policies

Avoid when:

  • TLS terminates elsewhere
  • you run many services behind one ingress
  • your forwarded header trust model is messy

Reverse proxy

Best when:

  • Nginx, Traefik, or Envoy terminates TLS
  • you want one consistent policy
  • platform or infra teams manage ingress

Avoid when:

  • every app truly needs custom behavior
  • you don’t control the proxy config

CDN or edge

Best when:

  • you already manage headers globally at the edge
  • you need consistency across many domains and apps

Avoid when:

  • you want developers to see security behavior in code
  • your edge/origin ownership is fragmented

Choosing the HSTS value

The header supports a few directives:

  • max-age=<seconds>
  • includeSubDomains
  • preload

Good starting point

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

That’s one year plus subdomain coverage. Solid production default if every subdomain is HTTPS-ready.

Safer rollout option

Strict-Transport-Security: max-age=300

Five minutes. Great for testing. Then bump to a day, then a month, then a year.

That gradual rollout matters more than people think. HSTS is sticky in browsers. If you push a bad policy, users keep feeling that pain long after you fix the server.

includeSubDomains: great until it isn’t

This directive is where a lot of breakage starts.

Pros

  • protects the whole domain tree
  • prevents weaker subdomains from becoming downgrade targets
  • usually the right long-term goal

Cons

  • breaks any subdomain still serving plain HTTP
  • impacts old admin panels, mail tools, forgotten dev hosts
  • can cause ugly surprises in larger organizations

If you don’t control every subdomain, don’t casually enable it. Audit first.

preload: strong protection, strong commitment

Preload puts your domain into browser-maintained HSTS preload lists, which means browsers enforce HTTPS even on the very first request.

Pros

  • closes the first-visit gap
  • strongest practical HSTS posture
  • great for mature HTTPS-only domains

Cons

  • hard to roll back
  • requires strict compliance across the domain
  • can break things badly if your subdomain hygiene is weak

Preload is not a checkbox for “more security points.” It’s a commitment. If you’re still cleaning up subdomains or your staging setup is chaotic, don’t do it yet.

A preload-style header looks like this:

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

That’s two years, which is typically expected for preload eligibility.

My opinionated recommendation for FastAPI teams

If you run FastAPI behind a reverse proxy, set HSTS at the proxy first.

Use FastAPI middleware only when:

  • you don’t have a reliable ingress layer for header management, or
  • you need app-specific behavior that the proxy can’t reasonably express

For rollout:

  1. start with max-age=300
  2. verify behavior in production
  3. move to max-age=86400
  4. then max-age=31536000
  5. add includeSubDomains only after auditing subdomains
  6. add preload only when you’re absolutely sure

That progression is boring, and boring is good in security headers.

Testing your FastAPI HSTS setup

You can verify manually:

curl -I https://example.com

You want to see:

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

You should also confirm the header is not being served over plain HTTP by the app layer in a misleading way. Usually the HTTP endpoint should redirect to HTTPS, and the HSTS header should appear on the HTTPS response.

For a quick check of HSTS and related headers, you can scan your site with HeaderTest.

A complete FastAPI example with optional middleware

If you do want app-level control, keep it explicit:

from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware

class HSTSMiddleware(BaseHTTPMiddleware):
    def __init__(self, app, max_age: int = 31536000, include_subdomains: bool = True, preload: bool = False):
        super().__init__(app)
        parts = [f"max-age={max_age}"]
        if include_subdomains:
            parts.append("includeSubDomains")
        if preload:
            parts.append("preload")
        self.hsts_value = "; ".join(parts)

    async def dispatch(self, request: Request, call_next):
        response = await call_next(request)
        forwarded_proto = request.headers.get("x-forwarded-proto")
        is_https = request.url.scheme == "https" or forwarded_proto == "https"

        if is_https:
            response.headers["Strict-Transport-Security"] = self.hsts_value

        return response

app = FastAPI()
app.add_middleware(HSTSMiddleware, max_age=300, include_subdomains=False, preload=False)

@app.get("/")
async def read_root():
    return {"ok": True}

That’s fine for development and controlled rollouts. I still wouldn’t treat it as a substitute for sane proxy configuration.

Final call: what should most teams do?

For a typical FastAPI deployment:

  • best default: set HSTS at the reverse proxy
  • best rollout: start with short max-age
  • best long-term policy: one year
  • use includeSubDomains carefully
  • use preload only when your domain is truly HTTPS-only everywhere

HSTS is simple on paper and unforgiving in production. That’s why I prefer the approach that matches where HTTPS is actually enforced. For most FastAPI stacks, that’s not the Python code. It’s the layer in front of it.

If you want the official references for framework and server behavior, check: