NextRush
API ReferenceMiddleware

@nextrush/rate-limit

Production-grade rate limiting with multiple algorithms and tiered limits.

Without rate limiting, a single client can exhaust your server's resources — intentionally or by accident. This middleware controls how many requests each client can make within a time window.

It supports three algorithms, tiered limits for different user types, CIDR-based whitelists and blacklists, and IETF-compliant response headers.


Default Behavior

With default options, rateLimit() applies these defaults:

SettingDefault
Algorithmtoken-bucket
Max requests100 per window
Window1m (60 seconds)
KeyClient IP with rl: prefix
Status code429
MessageToo many requests, please try again later.
Standard headersEnabled (RateLimit-*)
Legacy headersEnabled (X-RateLimit-*)
Retry-After headerEnabled on 429 responses

Installation

$ pnpm add @nextrush/rate-limit

Minimal Usage

import { createApp } from '@nextrush/core';
import { rateLimit } from '@nextrush/rate-limit';

const app = createApp();

// 100 requests per minute per IP, token-bucket algorithm
app.use(rateLimit());

Configuration Options

app.use(
  rateLimit({
    max: 1000,
    window: '15m',
    algorithm: 'sliding-window',
    trustProxy: true,
  })
);

RateLimitOptions

PropertyTypeDescription
algorithm'token-bucket' | 'sliding-window' | 'fixed-window'= 'token-bucket'Rate limiting algorithm
maxnumber= 100Maximum requests per window
windowstring | number= '1m'Window duration ('1s', '1m', '1h', '1d') or milliseconds
burstLimit?number= maxBurst limit for token bucket algorithm
keyGenerator?(ctx: Context) => string | Promise<string>Custom key generator function
skip?(ctx: Context) => boolean | Promise<boolean>Skip rate limiting for matching requests
store?RateLimitStore= In-memory storeCustom store implementation
handler?(ctx: Context, info: RateLimitInfo) => void | Promise<void>Custom handler for rate-limited requests
onRateLimited?(ctx: Context, info: RateLimitInfo) => void | Promise<void>Callback when a request is rate-limited
trustProxyboolean= falseTrust proxy headers for IP extraction
standardHeadersboolean= trueSend RateLimit-* headers
legacyHeadersboolean= trueSend X-RateLimit-* headers
includeRetryAfterboolean= trueSend Retry-After header on 429 responses
messagestring= 'Too many requests, please try again later.'Error message for rate-limited responses
statusCodenumber= 429HTTP status code for rate-limited responses
whitelist?string[]IPs to skip rate limiting (supports CIDR)
blacklist?string[]IPs to apply stricter limits (supports CIDR)
blacklistMultipliernumber= 0.5Multiplier for blacklisted IP limits (0–1)
draftIetfHeadersboolean= falseSend RateLimit-Policy header (IETF draft)
cleanupIntervalnumber= 60000Interval (ms) for expired entry cleanup
disableCleanupboolean= falseDisable automatic expired entry cleanup

Window Formats

The window option accepts duration strings or raw milliseconds:

'30s'; // 30 seconds
'1m'; // 1 minute
'15m'; // 15 minutes
'1h'; // 1 hour
'1d'; // 1 day
60000; // 1 minute (milliseconds)

Supported string units: s, sec, second, seconds, m, min, minute, minutes, h, hr, hour, hours, d, day, days.


Algorithms

Allows controlled bursts while maintaining an average rate:

app.use(
  rateLimit({
    algorithm: 'token-bucket',
    max: 100,
    window: '1m',
    burstLimit: 20,
  })
);

Typical use: Public APIs where occasional bursts are acceptable.

Smooth, accurate limiting using a weighted average of current and previous windows:

app.use(
  rateLimit({
    algorithm: 'sliding-window',
    max: 100,
    window: '1m',
  })
);

Typical use: Strict enforcement, billing-sensitive APIs, preventing boundary attacks.

Resets the counter at fixed intervals:

app.use(
  rateLimit({
    algorithm: 'fixed-window',
    max: 100,
    window: '1m',
  })
);

Typical use: Internal APIs, low-overhead use cases.

Clients can hit up to 2x the limit at window boundaries with fixed-window. Use sliding-window for strict enforcement.


Tiered Rate Limits

Apply different limits based on user type:

import { tieredRateLimit } from '@nextrush/rate-limit';

app.use(
  tieredRateLimit({
    tiers: {
      anonymous: { max: 60, window: '1m' },
      authenticated: { max: 1000, window: '1m' },
      premium: { max: 10000, window: '1m' },
    },
    tierResolver: (ctx) => ctx.state.user?.tier || 'anonymous',
    defaultTier: 'anonymous',
  })
);

TieredRateLimitOptions (extends RateLimitOptions without max/window)

PropertyTypeDescription
tiersRecord<string, TierConfig>Tier name → { max, window, burstLimit? }
tierResolver(ctx: Context) => string | Promise<string>Resolves the tier for a request
defaultTier?string= First key in tiersFallback tier if resolver returns an unknown name

Integration Example

Rate limiting with key generation, whitelist, and event logging:

import { createApp } from '@nextrush/core';
import { rateLimit } from '@nextrush/rate-limit';

const app = createApp();

app.use(
  rateLimit({
    max: 500,
    window: '15m',
    algorithm: 'sliding-window',
    keyGenerator: (ctx) => ctx.get('X-API-Key') || ctx.ip,
    whitelist: ['127.0.0.1', '10.0.0.0/8'],
    skip: (ctx) => ctx.path === '/health',
    onRateLimited: (ctx, info) => {
      console.log(`Rate limited: ${ctx.ip}, resets in ${info.resetIn}s`);
    },
  })
);

Response Headers

Headers are set on every response (not only 429 responses):

Standard headers (IETF draft):

RateLimit-Limit: 100
RateLimit-Remaining: 95
RateLimit-Reset: 30

Legacy headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640995200

The Retry-After header is included only on 429 responses.


Proxy Support

When your app runs behind a reverse proxy, the middleware needs to read forwarded IP headers to identify clients correctly.

Only enable trustProxy when your app runs behind a trusted reverse proxy. Untrusted clients can spoof IP headers to bypass rate limits.

app.use(rateLimit({ trustProxy: true }));

When enabled, the middleware checks these headers in priority order:

  1. CF-Connecting-IP (Cloudflare)
  2. X-Real-IP (Nginx)
  3. X-Forwarded-For (standard proxy)
  4. X-Client-IP (Apache)
  5. True-Client-IP (Akamai)
  6. X-Cluster-Client-IP (Rackspace)
  7. Forwarded-For
  8. Forwarded (RFC 7239)
  9. ctx.ip (socket address)

Custom Store

For distributed deployments, implement the RateLimitStore interface:

import { rateLimit, type RateLimitStore, type StoreEntry } from '@nextrush/rate-limit';

const redisStore: RateLimitStore = {
  async get(key: string): Promise<StoreEntry | null> {
    /* ... */
  },
  async set(key: string, entry: StoreEntry, ttlMs: number): Promise<void> {
    /* ... */
  },
  async increment(key: string, ttlMs: number): Promise<number> {
    /* ... */
  },
  async reset(key: string): Promise<void> {
    /* ... */
  },
  // Optional: decrement, cleanup, shutdown
};

app.use(rateLimit({ store: redisStore }));

Middleware Methods

The returned middleware exposes methods for programmatic control:

const limiter = rateLimit({ max: 100 });
app.use(limiter);

// Reset a specific key
await limiter.reset('rl:192.168.1.1');

// Get current rate limit info
const info = await limiter.getInfo('rl:192.168.1.1');

// Cleanup on shutdown
await limiter.shutdown();

Common Mistakes

Using trustProxy: true without a trusted proxy. Any client can send forged X-Forwarded-For headers. Only enable this behind a reverse proxy you control.

Setting window too short with high max. A window of '1s' with max: 1000 effectively disables rate limiting. Use longer windows with proportional limits.

Forgetting limiter.shutdown(). The in-memory store runs a cleanup timer. Call shutdown() during graceful server shutdown to clear the interval and free resources.

Using in-memory store across multiple instances. Each process has its own store. Requests load-balanced across instances each get full limits. Use a Redis-backed store for multi-instance deployments.


Troubleshooting

Rate limits reset on server restart. The default in-memory store does not persist state. Use a Redis or database-backed store for persistence.

Clients hit limits faster than expected. Check if trustProxy is set correctly. Without it, all proxied requests may share the same socket IP, causing shared limits.

RateLimitValidationError on startup. Options are validated at creation time. Check that max is a positive integer, blacklistMultiplier is between 0 and 1, and window uses a valid format.


On this page