Profiles·Public

express-rate-limit

semver>=6.0.0postconditions19functions3last verified2026-06-24coverage score100%

Postconditions: what we check

  • rateLimit · store-error-fail-closed
    error
    WhenThe backing store throws an error during hit counting (default passOnStoreError: false)
    ThrowsError forwarded to Express error handler via next(err)
    Required handlingApplication MUST register an Express error handler (4-argument middleware) to handle store failures. With default settings (fail-closed), a store error will block ALL requests until the error handler responds. Consider setting passOnStoreError: true for fail-open behavior in high-availability scenarios.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[1]
  • rateLimit · store-error-fail-open
    warning
    WhenThe backing store throws an error and passOnStoreError: true is configured
    ReturnsRequest is allowed through without rate limiting; error is logged to console
    Required handlingNo action required — use the returned value as needed.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[1]
  • rateLimit · rate-limit-exceeded
    info
    WhenClient has exceeded the configured limit within the windowMs time window
    ReturnsHTTP 429 response with configurable message (default: 'Too many requests, please try again later.') and Retry-After header
    Required handlingNo action required — use the returned value as needed.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[1]
  • rateLimit · undefined-ip
    error
    Whenrequest.ip is undefined (often due to missing trust proxy configuration)
    ThrowsLogs ERR_ERL_UNDEFINED_IP_ADDRESS warning; may result in incorrect rate limiting behavior
    Required handlingIf deploying behind a reverse proxy (nginx, Cloudflare, load balancer), caller MUST set app.set('trust proxy', N) in Express before using rateLimit(), where N is the number of trusted proxies. Failure to do so causes request.ip to be undefined or incorrect, allowing clients to bypass rate limiting by spoofing headers.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[2]
  • rateLimit · store-reuse
    error
    WhenA single store instance is shared across multiple rateLimit() middleware instances
    ThrowsLogs ERR_ERL_STORE_REUSE warning at startup
    Required handlingEach call to rateLimit() MUST use a separate store instance. Create a new store instance for each rate limiter to avoid shared hit count state between limiters.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[2]
  • rateLimit · success-within-limit
    info
    WhenClient is within the rate limit
    ReturnsRequest is forwarded to next middleware. X-RateLimit-* headers are added to the response.
    Required handlingNo action required — use the returned value as needed.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[1]
  • rateLimit · invalid-ip-address
    error
    Whenrequest.ip contains an invalid IPv4/IPv6 format or includes a port number (e.g. '127.0.0.1:3000'). Triggered by ERR_ERL_INVALID_IP_ADDRESS validation check.
    ThrowsLogs ERR_ERL_INVALID_IP_ADDRESS warning to console. The request is processed with the invalid key, which can cause bypass vulnerabilities — clients using port-mangled IPs are counted separately from normal IPs.
    Required handlingWhen deploying behind a proxy, ensure trust proxy is set correctly so Express normalizes request.ip. If using a custom keyGenerator, return ipKeyGenerator(req.ip) rather than raw req.ip to normalize the value. Invalid IP addresses can create bypass vulnerabilities that are invisible until exploited.
    costhighin prodsilent failureusers seesecurity breachvisibilitysilent
    Sources[2]
  • rateLimit · x-forwarded-for-without-trust-proxy
    error
    WhenX-Forwarded-For header is present in the request but Express trust proxy is not configured (default: false). Triggered by ERR_ERL_UNEXPECTED_X_FORWARDED_FOR validation check.
    ThrowsLogs ERR_ERL_UNEXPECTED_X_FORWARDED_FOR warning. Rate limiting uses the wrong IP — typically the load balancer's IP — causing all users to share a single rate limit bucket, effectively making the middleware a global limiter instead of a per-client limiter.
    Required handlingIf deploying behind a load balancer or reverse proxy that sets X-Forwarded-For, caller MUST configure app.set('trust proxy', N) where N is the number of trusted proxies in the chain. Without this, all clients appear to share the same IP, hitting the rate limit simultaneously.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[2]
  • rateLimit · double-count
    error
    WhenThe same request increments the rate limit counter more than once. Triggered by ERR_ERL_DOUBLE_COUNT validation — typically caused by duplicate rate limiter middleware instances applied to the same route, or by sharing a store instance without a unique prefix across multiple limiters.
    ThrowsLogs ERR_ERL_DOUBLE_COUNT warning. Clients hit their rate limit at half the configured threshold, causing legitimate users to be blocked unexpectedly.
    Required handlingReview route registration to ensure each rate limiter is applied exactly once per request path. If sharing an external store across multiple limiter instances, set a unique prefix per instance (e.g., store: new RedisStore({ prefix: 'api-limiter:' })).
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[2]
  • rateLimit · created-in-request-handler
    error
    WhenrateLimit() is called inside a request handler function (e.g., inside app.get() callback) rather than at application initialization time. Triggered by ERR_ERL_CREATED_IN_REQUEST_HANDLER.
    ThrowsLogs ERR_ERL_CREATED_IN_REQUEST_HANDLER warning at runtime. A new MemoryStore is created on each request, meaning hit counts are never accumulated — the rate limiter never actually limits anything.
    Required handlingAlways call rateLimit() at module/app initialization, not inside request handlers. The returned middleware instance should be created once and reused across requests. Example: const limiter = rateLimit({ ... }); app.use(limiter); // correct app.get('/route', (req, res) => { const limiter = rateLimit({ ... }); ... }); // wrong
    costhighin prodsilent failureusers seesecurity breachvisibilitysilent
    Sources[2]
  • rateLimit · rate-limit-invalid-store
    error
    WhenA store option is provided but is missing required interface methods: increment(), decrement(), or resetKey(). Also triggers if optional resetAll or init are present but are not functions.
    ThrowsTypeError("An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface.") — thrown synchronously during rateLimit() call at startup
    Required handlingCaller MUST ensure custom stores implement the full Store interface. Validate in integration tests. This throws synchronously at startup — the process crashes before any requests are served.
    costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[3][4]
  • rateLimit · async-callback-error-routes-to-handler
    error
    WhenAn async configuration callback (keyGenerator, skip, requestWasSuccessful, limit function, message function, or identifier function) throws or returns a rejected Promise during processing.
    ThrowsError forwarded to Express error handler via next(error). The middleware does NOT send an HTTP response — it calls next(error), routing to the nearest 4-argument Express error handler.
    Required handlingApplications MUST register a global Express error handler. Callback implementations should handle expected failures internally. Without a global error handler, Express sends a plain 500. Note: callback errors bypass passOnStoreError — they always route to next(error).
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[3][1]
  • rateLimit · store-init-failure-silently-logged
    error
    WhenA custom Store's init() method returns a rejected Promise (or throws synchronously) — e.g. Redis connection failed, schema migration crashed, namespace not provisioned. rateLimit() is called at app startup with this misconfigured store. (v8.5+ allows init() to be async.)
    ThrowsNo error is propagated to the rateLimit() caller. The init failure is logged via config.logger.error and swallowed. The middleware is returned and registered as if init succeeded. Subsequent requests invoke store.increment() against an uninitialized backend, which then fails per the configured passOnStoreError mode (default: block ALL requests, fail-closed).
    Required handlingCallers MUST NOT rely on rateLimit() to surface store-init failures. Either (1) await an explicit health-check on the store BEFORE calling rateLimit() at startup, or (2) supply a custom logger that escalates init errors to a process-crash or readiness-probe failure, or (3) accept that the first N requests after deploy will either 500 (default fail-closed) or bypass rate-limiting entirely (passOnStoreError: true). The default behavior — partial outage with only a log line — is the dangerous case.
    costhighin prodsilent failureusers seeservice unavailablevisibilitysilent
    Sources[3][5]
  • rateLimit · limit-zero-blocks-all-on-major-upgrade
    error
    WhenThe limit (or legacy max) option is set to 0 in a codebase that was originally written against express-rate-limit v6 or earlier, where limit: 0 meant "disable rate limiting." Common in feature-flag-driven configs (LIMIT=0 in dev/staging) and in code paths that conditionally disable rate-limiting via limit: featureFlagEnabled ? 5 : 0.
    ThrowsChangeWarning (WRN_ERL_MAX_ZERO) thrown during rateLimit() construction if the limit validation check is enabled. With validations disabled (validate: false or validate.limit: false in production), no warning fires — and limit: 0 now blocks every request with HTTP 429, where in v6 it allowed every request through.
    Required handlingCallers MUST audit any limit: 0 or max: 0 usage when upgrading from v6 or earlier. Replace with either a sentinel (limit: Infinity for "allow everything") or skip the rate limiter entirely via a conditional middleware registration. Do NOT disable the limit validation check in production — it is the only signal that catches this footgun at startup.
    costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[6][2]
  • rateLimit · ipv6-subnet-mask-changes-key-shape-on-upgrade
    error
    Whenv8 introduces a default ipv6Subnet of /56 — IPv6 client addresses are masked before keying. Apps upgrading from v7 to v8 that stored hit counters keyed by raw IPv6 addresses (e.g. in Redis with TTL) now have orphaned entries: the stored key is the raw address, but the v8 lookup key is the /56 subnet. resetKey(rawIpv6) silently no-ops because the stored key now uses the masked subnet. Existing per-IP allowlists keyed by raw v6 address also silently break.
    ThrowsNo error is thrown. Behavior changes silently. IPv6 clients that were previously rate-limited at per-address granularity now share counters across an entire /56 subnet (which on a hosting provider may span thousands of customers). Counter-resets via resetKey(rawAddr) silently fail because the key never matches. The validation system raises ERR_ERL_IPV6_SUBNET only for out-of-range manual ipv6Subnet values, NOT for the silent default-shift hazard.
    Required handlingApps upgrading v7 -> v8 with custom keyGenerator OR with admin endpoints that call resetKey() MUST audit the key format. Either (1) explicitly call ipKeyGenerator(req.ip) when constructing keys for resetKey/getKey lookups, OR (2) set ipv6Subnet: false to opt out of subnet masking (acceptable on IPv4-only deployments only), OR (3) clear the store on deploy to drop the v7 raw-IPv6 keys before traffic resumes. The keyGeneratorIpFallback validation catches custom keyGenerators that reference req.ip without going through ipKeyGenerator, but it does NOT catch out-of-band callers of resetKey/getKey passing raw addresses.
    costmediumin prodsilent failureusers seeauthentication failurevisibilitysilent
    Sources[7][1]
  • getKey · get-key-store-unsupported
    error
    WhenThe backing store does not implement the optional get() method. Many external store adapters (Redis, Memcached, PostgreSQL) do not implement get() by default.
    ThrowsThrows an Error: "The store does not support the 'get' method" (or equivalent from the adapter). Unhandled, this causes an unhandled promise rejection that crashes Node.js worker threads in production.
    Required handlingCallers of limiter.getKey() MUST wrap the call in a try-catch (or .catch()) to handle stores that do not implement get(). Since store support varies by adapter, defensive error handling is required regardless of which store adapter is in use. If the store doesn't support get(), fall back to allowing the request through or return a graceful degraded response.
    costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[4]
  • getKey · get-key-store-error
    error
    WhenThe backing store's get() method throws an error (e.g., Redis connection lost, database timeout) while limiter.getKey() is awaited without error handling.
    ThrowsStore error propagates as an unhandled promise rejection. Unlike the middleware itself (which has passOnStoreError for fail-open behavior), getKey() has no built-in error isolation — the raw store error is thrown directly to the caller.
    Required handlingWrap getKey() in try-catch. On store error, implement a fail-open fallback (return default quota info or skip the pre-check) rather than blocking the request. Example: try { info = await limiter.getKey(key); } catch { info = undefined; }
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[1]
  • resetKey · reset-key-unprotected-endpoint
    error
    WhenThe reset endpoint itself is subject to the same rate limiter it is trying to reset. A client that is already rate-limited (HTTP 429) cannot reach the reset handler — the rate limit middleware blocks the request before resetKey() is ever called.
    ThrowsNo error is thrown — the issue is architectural. resetKey() is never reached because the rate limiter blocks the request first. The unlock operation silently fails from the client's perspective.
    Required handlingAny endpoint that calls resetKey() MUST be exempt from the same rate limiter. Use the skip option: skip: (req) => req.path === '/admin/reset-limit' Or apply the rate limiter only to specific routes rather than globally.
    costmediumin prodsilent failureusers seeauthentication failurevisibilitysilent
    Sources[1]
  • resetKey · reset-key-wrong-key-format
    warning
    WhenresetKey() is called with a raw IP address string when the rate limiter was configured with a custom keyGenerator that produces a different key format (e.g., user ID, API key, or ipKeyGenerator() with IPv6 subnet masking). The key does not match any stored entry.
    ThrowsNo error is thrown — the operation silently no-ops. The client's hit count is not reset. This is a silent failure that is particularly dangerous in account unlock flows.
    Required handlingAlways call resetKey() with the same key format that keyGenerator produces. If using IP-based limiting, call ipKeyGenerator(req.ip) to normalize before passing to resetKey(). If using user-ID-based limiting, pass the user ID. Verify the key format matches by testing with getKey() after calling resetKey() to confirm the counter was actually cleared.
    costlowin prodsilent failureusers seeauthentication failurevisibilitysilent
    Sources[4]

Sources

Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.

Source code
Other references
  • [1]
    express-rate-limit.mintlify.app/reference/configuration
    Configuration
  • [2]
    express-rate-limit.mintlify.app/reference/error-codes
    Error Codes
  • [4]
    express-rate-limit.mintlify.app/guides/creating-a-store
    Creating A Store
  • [5]
    express-rate-limit.mintlify.app/reference/changelog
    Changelog

Research notes

Curator notes from SOURCES.md captured when the profile was written so you can verify the reasoning, not just the rules.

Sources: express-rate-limit

Official Documentation

Research Date: 2026-02-26

Need a different package?
Request a profile