Known Limitations
Every framework has edges. This page collects the ones worth knowing before you ship, in one place, so you don’t discover them in production. Each item links to the page that covers it in depth — nothing here is new behavior, it’s the honest fine print pulled together.
The theme is the same across most of them: several built-in protections keep their state in the process, not in a shared store. That’s fast and zero-config for a single instance, and exactly what you want in development. It stops being a single source of truth the moment you run more than one replica.
Single-instance state
Section titled “Single-instance state”These features are correct on one node and degrade silently when you scale horizontally. None of them error — they just enforce a weaker guarantee than you might assume.
- Per-route rate limiting (
middleware.rateLimit) is process-local. Each replica counts independently, so withNreplicas the effective cap isN × max. For a genuinely shared limit, configure@fastify/rate-limit’s Redis store throughhttp.rateLimit. See Security → the protective middleware. - Concurrency limiting (
middleware.concurrencyLimit) is process-local too. The in-flight cap is per-process. For a cluster-wide ceiling, reach for a distributed lock via@warlock.js/cache. See Security. - Idempotency (
middleware.idempotency) dedupes against the configured cache. If that cache is the in-memory driver, a retry that lands on a different replica won’t see the first request’s key and will execute twice. Point idempotency at a shared cache (Redis) in production. See Security and theEC100/EC101codes in Error handling. - The global rate limit (
http.rateLimit) is a separate, coarser backstop from the per-route middleware — and it has the same single-instance store caveat unless you wire a shared store. See Security → the global rate limit is separate.
Rule of thumb: if a protection counts requests or remembers keys, and you run more than one instance, give it a shared store (Redis) or treat its guarantee as best-effort.
Transport & proxy trust
Section titled “Transport & proxy trust”- Fixed in 4.13.0 —
http.corsnow applies. Up to and including 4.12.0 the framework’s defaults ({ origin: "*", methods: "*" }) were spread after your configuration, sohttp.corscould not tightenorigin/methodsat all — an app that configured an allow-list still answered every origin. Your configuration now wins. If you are upgrading and were unknowingly running open CORS, this will start rejecting origins. See Security → CORS. - Fixed in 4.13.0 —
X-Forwarded-For/X-Real-IPare no longer trusted by default.http.trustProxynow defaults tofalse, sorequest.detectIp(),middleware.ipFilter()and IP-scoped rate limiting read the socket address. Previously the default wastrue, which meant a client could send its own forwarding header and be believed — and because@fastify/rate-limitkeys its buckets onrequest.ip, a different header per request produced a fresh bucket each time. Sethttp.trustProxy: trueonly when you are genuinely behind a proxy that overwrites those headers. See Security. - Since 4.15.0 — bare
trueis rarely the right shape. An edge that appends toX-Forwarded-For(nginx, an ALB, most CDNs) leaves whatever the client prepended as the leftmost entry, sotrustProxy: true(“trust the leftmost hop”) can hand the client its own chosen IP straight back.http.trustProxynow also accepts a hop count (2) or a trusted-proxy CIDR/IP list, both delegated to Fastify and honoured bydetectIp().X-Real-IPis only honoured undertrustProxy: true— it carries no chain to check a hop count or list against. See Security →trustProxyshapes.
API surface
Section titled “API surface”- There is no OpenAPI / Swagger generator yet. seal schemas carry
.describe(...)metadata and JSON-schema output, and validation is declarative, but the framework does not emit an OpenAPI document from your routes today. If you need a spec, generate it from your schemas yourself. See Validation.
Repository nuances
Section titled “Repository nuances”listCached/getCachedcache against the configured cache driver. On the in-memory driver the cache is per-process, so two replicas can serve different cached snapshots until each expires. Use a shared cache driver if read-after-write consistency across instances matters. See Repositories and Cache.request.all()feeds the repository’sfilterBydirectly. Only the keys you declare infilterBybecome query filters — undeclared inputs are ignored, which is the intended safety boundary, but it also means a typo’d filter key silently does nothing rather than erroring. See Repositories.
Shutdown
Section titled “Shutdown”SIGKILLskips graceful shutdown entirely. Reverse-order connector teardown only runs forSIGINT/SIGTERM(andSIGHUPon Windows). A hard kill gives connectors no chance to close — configure your supervisor to send a terminable signal with a short grace period. See Deployment and Health checks.
See also
Section titled “See also”- Security — the protective middleware, the CORS default, proxy trust, and the full hardening checklist.
- Production Hardening Checklist — the actionable counterpart to this page: what to set before you ship.
- Deployment & production — build/start, environment selection, graceful shutdown.
- Error handling — the
HttpErrorCodescatalog, including the idempotency codes. - Health checks —
/health,/ready, and request draining on shutdown.