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)

With headers:

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).
Do not key an anonymous request on a raw X-Forwarded-For / X-Real-IP value. Those headers are attacker-supplied — a client can put any address in them and rotate it every request to get a fresh bucket, bypassing the limiter entirely. The IP must come from a source the caller cannot forge. See below.

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:
If the gateway keyed on the socket peer, every request in the world would share one bucket (the proxy’s address) and a single busy client would rate-limit everybody. So the real client IP must be recovered from the request — but safely, because the obvious source (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:
The Fly deployment is self-configuring: 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 silence 429s entirely during testing (e.g. an emulator retry loop), disable the limiter:
The boot-guard only fires in production, so this is always safe on a dev machine.

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

  1. rate_limit_exceeded_total — Counter of denials
  2. rate_limit_remaining — Gauge of remaining requests
  3. rate_limit_reset_seconds — Time until window reset

Security Considerations

Best Practices

  1. Set generous limits — Users shouldn’t hit limits during normal use
  2. Communicate limits — Document in API docs
  3. Include headers — Let clients track their usage
  4. Fail-open — Allow requests if Redis unavailable
  5. Monitor — Track exceeded events in production