Rate Limiting
Production-ready rate limiting using Bun’s native Redis client with sliding window algorithm.Overview
Response Headers
Every response includes rate limit info:Rate Limited Response (429)
How It Works
Sliding Window Algorithm
Identifier Selection
The key is what the limiter counts requests against. Authenticated requests key on the user; everyone else keys on their client IP (see Client IP behind a proxy for how that IP is resolved safely).Client IP behind a proxy
The gateway never talks to end users directly — a proxy (Fly Proxy in production, and often a reverse proxy elsewhere) sits in front of it. That changes what “the client IP” means and, if handled naively, breaks rate limiting.The problem
The network socket only tells the server who it is directly connected to. Behind a proxy, that is always the proxy, not the real client:X-Forwarded-For) is caller-controlled.
How the gateway resolves it
clientAddress() tries three sources, in order, and stops at the first that applies:
1
Trusted platform header (preferred)
A header the fronting proxy overwrites on every request with the real
client IP. Because the proxy replaces whatever the caller sent, it cannot be
spoofed and is used verbatim — no socket check needed. Configured via
CLIENT_IP_HEADER. On Fly.io this is Fly-Client-IP.2
x-forwarded-for, resolved from the RIGHT
Only if the socket peer is an allowlisted proxy
(
TRUSTED_PROXY_IPS). X-Forwarded-For grows
left→right as spoofable…, realClient; the proxy appends the address it
observed, so the real client is on the right. The gateway walks from the
right, discards allowlisted proxy hops, and takes the first untrusted hop.
Picking the leftmost entry (index 0) would let the caller choose its own key.3
Socket address (fallback)
A direct client with no trusted header and no allowlisted proxy in front is
keyed on its socket address — the only value it cannot forge.
Why the platform header can’t be faked.
Fly-Client-IP is set by Fly
Proxy itself and overwrites any value the caller supplies — unlike
X-Forwarded-For, which Fly appends to. That guarantee is why the header can
be trusted verbatim. See Fly request headers.Production boot-guard
Because a misconfigured proxy boundary silently collapses all traffic into one bucket, the gateway refuses to start in production when rate limiting is on but neither a trusted header nor a proxy allowlist is configured:fly.toml sets CLIENT_IP_HEADER = 'Fly-Client-IP', so a correct deploy needs nothing extra.
Local development
There is no proxy locally, so the socket address is already the real client and no client-IP config is needed. To silence429s entirely during testing (e.g. an emulator retry loop), disable the limiter:
Configuration
Environment Examples
Endpoint-Specific Limits
Override limits per endpoint in route config:Skip Rate Limiting
Internal service calls can skip rate limiting:Redis Key Schema
Performance
- Redis O(log N) for sorted set operations
- < 1ms typical latency
- Atomic operations (no race conditions)
- No external calls if Redis unavailable (fail-open by default)
Monitoring
Log Rate Limit Events
Metrics to Track
rate_limit_exceeded_total— Counter of denialsrate_limit_remaining— Gauge of remaining requestsrate_limit_reset_seconds— Time until window reset
Security Considerations
Best Practices
- Set generous limits — Users shouldn’t hit limits during normal use
- Communicate limits — Document in API docs
- Include headers — Let clients track their usage
- Fail-open — Allow requests if Redis unavailable
- Monitor — Track exceeded events in production