@fastify/rate-limit

repository·main·Indexed 20 days ago

https://github.com/fastify/fastify-rate-limit

A low overhead rate limiter plugin for Fastify version 11.2.0 designed to prevent abuse and brute-force attacks. It supports in-memory, Redis, and custom storage backends, providing capabilities for global or route-specific limits, custom error responses, and manual rate limiting via createRateLimit().

Tokens
4K
Snippets
11
Records
15
Agent score
20%

What's inside @fastify/rate-limit

  1. Basic Usage of @fastify/rate-limit

    main

    To use the plugin, register it with your Fastify instance. By default, it adds an onRequest hook that checks the client's IP address against a configured timeWindow and max request limit. If the limit is exceeded, the client receives a 429 Too Many Requests response.

    import Fastify from 'fastify'
    
    const fastify = Fastify()
    await fastify.register(import('@fastify/rate-limit'), {
      max: 100,
      timeWindow: '1 minute'
    })
    
    fastify.get('/', (request, reply) => {
      reply.send({ hello: 'world' })
    })
    
    fastify.listen({ port: 3000 }, err => {
      if (err) throw err
      console.log('Server listening at http://localhost:3000')
    })
  2. Customize the Rate Limit Error Response

    main

    When a client hits the rate limit, you can customize the response in two ways:

    1. Using errorResponseBuilder: Provide a function during plugin registration to return a custom object.
    2. Using a custom error handler: Use Fastify's setErrorHandler to intercept the 429 status code and modify the response.

    Default error response structure:

    {
      "statusCode": 429,
      "error": "Too Many Requests",
      "message": "Rate limit exceeded, retry in 1 minute"
    }
    await fastify.register(import('@fastify/rate-limit'), {
      errorResponseBuilder: function (request, context) {
        return {
          statusCode: 429,
          error: 'Too Many Requests',
          message: `I only allow ${context.max} requests per ${context.after} to this Website. Try again soon.`,
          date: Date.now(),
          expiresIn: context.ttl // milliseconds
        }
      }
    })
  3. Configure Rate Limiting on Specific Endpoints

    main

    You can override global plugin settings for specific routes using the config.rateLimit object in the route definition.

    • To disable rate limiting on a route that has global limiting enabled, set rateLimit: false.
    • To override settings, provide a rateLimit object with specific max, timeWindow, groupId, etc.
    • Use groupId to group multiple routes together for a shared per-group rate limit.
    // Disable rate limiting for a specific route
    fastify.get('/public', {
      config: {
        rateLimit: false
      }
    }, (request, reply) => {
      reply.send({ hello: 'no limits here' }})
    })
    
    // Override settings and use a groupId
    fastify.get('/otp/send', {
      config: {
        rateLimit: {
          max: 3,
          timeWindow: '1 minute',
          groupId: 'OTP'
        }
      }
    }, (request, reply) => {
      reply.send({ hello: 'grouped limit' }})
    })
  4. Preventing URL Guessing via 404 Rate Limiting

    main

    To prevent attackers from brute-forcing URLs via 404 errors, you can apply rate limiting to your Not Found handler. You can use the plugin's preHandler capability within setNotFoundHandler.

    const fastify = Fastify()
    await fastify.register(rateLimit, { global: true, max: 2, timeWindow: 1000 })
    
    fastify.setNotFoundHandler({
      preHandler: fastify.rateLimit({
        max: 4,
        timeWindow: 500
      })
    }, function (request, reply) {
      reply.code(404).send({ hello: 'world' })
    })
  5. Configure the rate limit storage (Store)

    main

    The plugin supports different storage backends to track request counts:

    1. LocalStore (Default): Uses in-memory storage. Best for single-instance applications.
    2. RedisStore: Uses Redis for distributed rate limiting across multiple server instances. To use this, provide a redis connection object in the plugin settings.
    3. Custom Store: You can provide your own store implementation via the store option. A custom store must implement an incr(key, callback, timeWindow, max) method. If using { increment: false } in manual calls, the store must also implement a read(key, callback, timeWindow, max) method.
    // Example using Redis
    fastify.register(rateLimit, {
      redis: new Redis({ host: '127.0.0.1' }),
      nameSpace: 'my-app-ratelimit'
    })
  6. Configure @fastify/rate-limit Plugin Options

    main

    The following options can be passed during fastify.register():

    OptionTypeDefaultDescription
    globalbooleantrueApply to all routes in the encapsulation scope
    maxnumber or async function1000Max requests per timeWindow. Function signature: async (request, key) => number
    bannumber-1Max 429 responses before returning 403. Set to 0 to always return 403 on exceed
    timeWindownumber or string or async function60000Duration in ms, ms string, or async (request, key) => number
    cachenumber5000LRU cache size
    allowListstring[] or function[]IPs or function (request, key) => boolean to exclude from limiting
    redisioredis instancenullExternal store for multi-server setups
    nameSpacestring'fastify-rate-limit-'Redis key prefix
    continueExceedingbooleanfalseRenew limitation when user sends request while still limited
    skipOnErrorbooleanfalseSkip errors generated by storage (e.g. Redis down)
    keyGeneratorfunction(request) => normalizeIP(...)Function to generate unique identifier per request
    ipv6Subnetnumber64IPv6 prefix length for default keyGenerator
    errorResponseBuilderfunction(request, context) => objectCustom response generator
    enableDraftSpecbooleanfalseUse IETF draft header standard
    addHeadersOnExceedingobjectAll headersHeaders to show when limit is NOT reached
    addHeadersobjectAll headersHeaders to show when limit IS reached
    storeClassIn-memoryCustom storage mechanism
    onExceedingfunctionnullCallback before limit is reached
    onExceededfunctionnullCallback after limit is reached
    onBanReachfunctionnullCallback when ban limit is reached
    exponentialBackoffbooleanfalseRenew limitation exponentially when user requests while limited
  7. Configure rate limits on specific routes

    main

    You can override global settings by adding a rateLimit object to the config property of a route definition. This allows for granular control (e.g., stricter limits on sensitive endpoints like /login).

    Supported configuration keys in routeOptions.config.rateLimit include all global options mentioned in the registration settings, plus groupId (a string used to append to the rate limit key to create separate buckets for the same IP/user).

    To disable rate limiting for a specific route, set rateLimit: false in the route config.

    fastify.get('/sensitive', {
      config: {
        rateLimit: {
          max: 5,
          timeWindow: '1 minute',
          groupId: 'login-attempts'
        }
      }
    }, async (request, reply) => {
      return { status: 'ok' }
    })
  8. Register the @fastify/rate-limit plugin

    main

    To use rate limiting in your Fastify application, register the @fastify/rate-limit plugin. You can provide global configuration settings during registration that will apply to all routes unless overridden by specific route configurations.

    const fastify = require('fastify')()
    const rateLimit = require('@fastify/rate-limit')
    
    fastify.register(rateLimit, {
      max: 1000,
      timeWindow: '1 minute'
    })
  9. Manual Rate Limiting with createRateLimit()

    main

    For use cases like GraphQL or tRPC where you need manual control, use fastify.createRateLimit(). This returns a function that accepts a FastifyRequest and returns a limit object.

    Limiter Signature: const limit = await checkRateLimit(request, { increment?: boolean })

    The increment option:

    • true (default): Consumes a request from the quota.
    • false: Returns a non-mutating snapshot of the current status (a "peek") without consuming a request. Useful for checking status before a sensitive operation (like login) and only incrementing on failure.

    Returned limit object properties:

    • isAllowed: true if excluded via allowList.
    • key: The generated identifier.
    • isExceeded: true if the limit was reached.
    • isBanned: true if the client is banned.
    • max, timeWindow, remaining, ttl, ttlInSeconds.
    const checkRateLimit = fastify.createRateLimit({ max: 5, timeWindow: '1 minute' });
    
    fastify.post('/login', async (request, reply) => {
      // Peek at the current status without consuming a request
      const status = await checkRateLimit(request, { increment: false });
      if (status.isExceeded) {
        return reply.code(429).send({ error: 'Too many attempts' });
      }
    
      const success = await tryLogin(request.body);
      if (!success) {
        // Only consume a request when the login fails
        await checkRateLimit(request);
        return reply.code(401).send({ error: 'Invalid credentials' });
      }
    
      return { ok: true };
    });
  10. Configure global rate limit settings

    main

    When registering the plugin, you can define several global options:

    • max: The maximum number of requests allowed within the timeWindow. Can be a number or a function(req, key). Defaults to 1000.
    • timeWindow: The duration of the rate limit window. Can be a number (milliseconds), a string (e.g., '1 minute', '5m'), or a function(req, key). Defaults to 60000.
    • hook: The Fastify lifecycle hook to use. Defaults to 'onRequest'.
    • allowList: A list of keys (e.g., IPs) to exempt from rate limiting. Can be an Array or a function(req, key) that returns a boolean.
    • ban: The number of requests allowed beyond the max before the user is banned. Defaults to -1 (no banning).
    • onBanReach: A callback function triggered when a user reaches the ban threshold. (req, key) => void.
    • onExceeding: A callback function triggered when a user exceeds max but hasn't reached the ban threshold. (req, key) => void.
    • onExceeded: A callback function triggered when a user is rate limited. (req, key) => void.
    • continueExceeding: If true, the request continues even if the limit is exceeded. Defaults to false.
    • exponentialBackoff: If true, enables exponential backoff logic. Defaults to false.
    • ipv6Subnet: The subnet mask for IPv6 addresses to group users. Defaults to 64.
    • keyGenerator: A function to generate the rate limit key. (req) => Promise<string> | string. Defaults to using the request IP.
    • errorResponseBuilder: A function to customize the error thrown when rate limited. (req, context) => Error. The context contains statusCode, ban, max, ttl, and after (a human-readable string).
    • skipOnError: If true, errors from the store will be caught and ignored. Defaults to false.
    • enableDraftSpec: If true, uses the draft specification header names (e.g., ratelimit-limit instead of x-ratelimit-limit).
    • addHeaders: Custom headers to add to the response. Defaults to standard x-ratelimit-* headers.
    • addHeadersOnExceeding: Custom headers to add specifically when the limit is exceeded.
  11. Rate Limit Response Headers

    main

    The plugin adds headers to the response to inform the client about their current status.

    Standard Headers (Default):

    • x-ratelimit-limit: Total requests allowed in the window.
    • x-ratelimit-remaining: Requests remaining in the current window.
    • x-ratelimit-reset: Seconds remaining until the limit resets.
    • retry-after: (Only when limit is reached) Seconds to wait before retrying.

    IETF Draft Spec Headers (if enableDraftSpec: true):

    • ratelimit-limit
    • ratelimit-remaining
    • ratelimit-reset
    • retry-after