rateLimiters

In-memory fixed-window, sliding-window, and sliding-window-counter rate limiters backed a memory cache.

The rateLimiters module provides three lightweight, in-memory rate limiting functions for per-process traffic control. Each factory accepts cache configuration once and returns a synchronous limiter function that evaluates a string key such as an IP address, user ID, API key, or composite identifier.

All three functions store state in MiniCache, so idle keys expire automatically. The returned limiter resolves to true when the request is accepted and false when the current request exceeds the configured limit.

These limiters keep all state in local memory. They do not synchronize across multiple Node.js processes, containers, or regions, and they do not generate HTTP headers such as Retry-After.

Definition

The module exports three factory functions and a small set of interfaces that describe the cache payload used by each strategy.

rateLimiters.ts
export interface Entry {
  count: number
  windowStart: number
}

export interface RateEntry {
  timestamps: number[]
}

export interface CounterEntry {
  currentBucket: number
  previousBucket: number
  bucketStart: number
}

export interface CacheConfig {
  maxEntries?: number
  sweepIntervalMs?: number
}

export function fixedWindowRateLimiter(
  cache: CacheConfig
): (key: string, limit?: number, windowMs?: number) => boolean

export function slidingWindowRateLimiter(
  cache: CacheConfig
): (key: string, limit?: number, windowMs?: number) => boolean

export function slidingWindowCounterRateLimiter(
  cache: CacheConfig
): (key: string, limit?: number, windowMs?: number) => boolean

State interfaces

The exported interfaces are the internal shapes stored in MiniCache. You do not need them for everyday use, but they explain how each algorithm tracks recent traffic.

InterfaceUsed byDescription
EntryfixedWindowRateLimiterStores the accepted request count and the timestamp where the active fixed window started.
RateEntryslidingWindowRateLimiterStores every accepted request timestamp that still belongs to the active rolling window.
CounterEntryslidingWindowCounterRateLimiterStores the current bucket count, the previous bucket count, and the start time of the current bucket.

Shared configuration

Each factory accepts the same CacheConfig object. These options control only how limiter state is stored and cleaned up in memory.

OptionTypeDefaultDescription
maxEntriesnumberInfinityMaximum number of active keys kept in memory. When this limit is reached, MiniCache evicts the oldest key and that key starts fresh on its next request.
sweepIntervalMsnumber60000Interval in milliseconds used by MiniCache to remove expired keys in the background. Access-time checks still enforce expiry even if the sweep has not run yet.

Choosing a strategy

The three factories share the same public signature, but they trade accuracy, memory use, and burst handling differently.

StrategyAccuracyMemory per keyBest forTradeoff
fixedWindowRateLimiterLowestLowestSimple IP throttling and coarse route protectionBoundary bursts can pass at the end of one window and the start of the next.
slidingWindowRateLimiterHighestHighestSensitive endpoints such as login, reset, or verification routesStores every accepted timestamp for the active window.
slidingWindowCounterRateLimiterHigh, approximateLowHigh-throughput APIs where timestamp arrays are too expensiveUses weighted buckets, so the count is an approximation.

Returned limiter function

Each factory returns the same limiter signature. The function is synchronous, so you can call it directly inside middleware, route handlers, or in-memory guards.

ParameterTypeRequiredDescription
keystringYesUnique identity to limit, such as an IP address, user ID, session ID, API key, or composite key.
limitnumberNoMaximum number of accepted requests inside the active window. Defaults to 50.
windowMsnumberNoWindow size in milliseconds. Defaults to 1000.

The returned function resolves immediately to true when the request is accepted, or false when the request must be rejected.

Methods

The following factory functions differ only in how they count recent traffic for each key.

fixedWindowRateLimiter(cache)

fixedWindowRateLimiter stores a single counter and a windowStart timestamp per key. The window is anchored to the first accepted request for that key, and the counter resets when the full window duration has elapsed.

This is the lightest strategy in memory and CPU cost. It works well for coarse protection, but it can admit bursts around a window boundary.

ParameterTypeRequiredDescription
cacheCacheConfigYesCache settings for limiter state. See the shared configuration table above.
Returned limiter parameterTypeRequiredDescription
keystringYesUnique subject to track.
limitnumberNoMaximum accepted requests for the current fixed window. Defaults to 50.
windowMsnumberNoFixed window duration in milliseconds. Defaults to 1000.

Use this strategy when you need fast, predictable protection with minimal overhead, such as basic per-IP throttling on a public route.

loginLimiter.ts
import { fixedWindowRateLimiter } from '@riavzon/utils'

const limitLoginAttempts = fixedWindowRateLimiter({
  maxEntries: 50_000,
  sweepIntervalMs: 30_000,
})

export function canAttemptLogin(ip: string) {
  return limitLoginAttempts(ip, 5, 60_000)
}

slidingWindowRateLimiter(cache)

slidingWindowRateLimiter stores an array of accepted timestamps for each key. On every request, it removes timestamps older than windowMs, counts the remaining timestamps, and accepts or rejects the request from that rolling set.

This strategy gives the most accurate rolling-window behavior. It smooths out boundary bursts, but it uses more memory because every accepted request adds a timestamp to the active window.

ParameterTypeRequiredDescription
cacheCacheConfigYesCache settings for limiter state. See the shared configuration table above.
Returned limiter parameterTypeRequiredDescription
keystringYesUnique subject to track.
limitnumberNoMaximum accepted requests inside the rolling window. Defaults to 50.
windowMsnumberNoSliding window size in milliseconds. Defaults to 1000.

Use this strategy for endpoints where fairness matters more than raw throughput, such as login, password reset, or OTP verification flows.

passwordResetLimiter.ts
import { slidingWindowRateLimiter } from '@riavzon/utils'

const limitPasswordReset = slidingWindowRateLimiter({
  maxEntries: 100_000,
  sweepIntervalMs: 60_000,
})

export function canSendResetEmail(key: string) {
  return limitPasswordReset(key, 3, 15 * 60_000)
}

slidingWindowCounterRateLimiter(cache)

slidingWindowCounterRateLimiter stores only the current bucket count, the previous bucket count, and the current bucket start time. It estimates the effective request count by weighting the previous bucket according to how far the current time has progressed through the active window.

This strategy is a good balance between accuracy and memory use. It avoids the timestamp array used by a full sliding window while still reducing the sharp boundary effects of a fixed window.

ParameterTypeRequiredDescription
cacheCacheConfigYesCache settings for limiter state. See the shared configuration table above.
Returned limiter parameterTypeRequiredDescription
keystringYesUnique subject to track.
limitnumberNoMaximum estimated requests inside the weighted window. Defaults to 50.
windowMsnumberNoBucket size and effective window size in milliseconds. Defaults to 1000.

Use this strategy for high-volume APIs where you need smoother throttling than a fixed window, but storing every timestamp would create too much overhead.

apiKeyLimiter.ts
import { slidingWindowCounterRateLimiter } from '@riavzon/utils'

const limitApiKey = slidingWindowCounterRateLimiter({
  maxEntries: 200_000,
  sweepIntervalMs: 10_000,
})

export function canCallApi(apiKey: string) {
  return limitApiKey(apiKey, 100, 60_000)
}

Example usage

In a typical HTTP handler, call the limiter before you perform the expensive or security-sensitive work. If the limiter returns false, return a 429 response immediately.

serverHandler.ts
import { fixedWindowRateLimiter } from '@riavzon/utils'

const limitRequests = fixedWindowRateLimiter({ maxEntries: 25_000 })

export async function handleRequest(req: Request, ip: string) {
  if (!limitRequests(ip, 20, 60_000)) {
    return new Response('Too Many Requests', { status: 429 })
  }

  return new Response(JSON.stringify({ ok: true }), { status: 200 })
}

These rate limiters are often used with other shared utilities in the package. The following pages explain the closest related building blocks.

  • See MiniCache for the TTL and eviction behavior that backs these limiters.
  • See fetchWithRetry if you also need client-side backoff and retry behavior after a 429 response.
Logo