Advanced

Rate Limiter

Restrict API request rates per client IP, return 429 responses with Retry-After headers, and share counters across clusters with Redis.

VitNode includes automated IP-based rate limiting middleware. Exceeding the request budget immediately returns 429 Too Many Requests with a Retry-After header, protecting authentication and public endpoints from brute-force attacks.

Quick start

Customize the rate limiter budget in apps/api/src/vitnode.api.config.ts:

apps/api/src/vitnode.api.config.ts
import { buildApiConfig } from "@vitnode/core/vitnode.config"

export const vitNodeApiConfig = buildApiConfig({
  rateLimiter: {
    points: 60, // Max requests allowed
    duration: 60, // Window in seconds
  },
})

The default configuration allows 80 requests per 60 seconds per IP address.

Disabled in development

Rate limiting is automatically bypassed when NODE_ENV=development to prevent interruptions during local coding.


429 Error Response Format

When a client exhausts their request allowance, the API returns a structured JSON error:

HTTP/1.1 429 Too Many Requests
Retry-After: 24
Content-Type: application/json

{
  "error": "Too Many Requests",
  "retryAfter": 24
}

Distributed Rate Limiting (Redis)

  • Without Redis: Counters are tracked in local memory per server instance.
  • With Redis: Counters are synchronized across all API containers using a distributed sliding window.

Configure Redis in vitnode.api.config.ts:

export const vitNodeApiConfig = buildApiConfig({
  redis: process.env.REDIS_URL
    ? { url: process.env.REDIS_URL, password: process.env.REDIS_PASSWORD }
    : undefined,
})

Real visitor IPs for server-rendered pages

When your web app renders a page on the server, it calls the API itself - so the API sees the web server's address, not your visitor's. Every server-rendered request then lands in the same bucket, and 80 requests a minute disappears fast on a busy site.

The API never believes a plain X-Forwarded-For header (anyone can type one). Instead, the web server works out the visitor's address, signs it with a key derived from your CRON_SECRET - the secret the web app and the API already share - and the API checks that signature before trusting it.

  • Set the same CRON_SECRET in both apps (you need it there anyway).
  • Tell the web app how many proxies sit in front of it with VITNODE_TRUSTED_PROXY_HOPS. It skips exactly that many hops from the right of X-Forwarded-For and ignores everything further left, because a visitor can write whatever they like there.
  • The API trusts the forwarded address only when the signature matches and is less than 5 minutes old.
  • While CRON_SECRET is unset or still a published placeholder, nothing is trusted: the API keys every request on the connection address.
SetupVITNODE_TRUSTED_PROXY_HOPS
Web app exposed directly, no proxy (default)0
One reverse proxy (Nginx, Caddy, a load balancer, Vercel)1
A CDN in front of your reverse proxy2

Count your proxies, not your hopes

A number higher than the proxies you actually run lets visitors choose their own address again. With the default 0, every server-rendered request behind a proxy shares the proxy's bucket, so the limit is safe but tight.

One secret, two jobs

Anyone holding CRON_SECRET can trigger cron jobs and pick any IP address the rate limiter will believe. Use a long random value and rotate it in both apps together.


rateLimiter Options

Prop

Type

Learn More