Rate Limiting API Reference

getConsumerIdentifiers

Configure the rate limiting to understand which consumers to group into the same rate limiting bucket. Note that returned values are always hashed before being used as a rate limiting bucket.

A common example is to rate limit authenticated users based on the authentication token and public users based on the IP address:

import { Config } from 'stellate'

const config: Config = {
  config: {
    getConsumerIdentifiers: (req) => {
      return {
        user: req.headers.authorization || req.ip,
      }
    },
  },
}
export default config

You can then select which consumer identifier to use with the rateLimit.consumerIdentifier option:

import { Config } from 'stellate'

const config: Config = {
  config: {
    getConsumerIdentifiers: (req) => {
      return {
        user: req.headers.authorization || req.ip,
      }
    },
    rateLimits: [
      {
        // Use the "user" consumer identifier
        consumerIdentifier: 'user',
        name: 'IP limit',
        limit: {
          type: 'RequestCount',
          budget: 10,
          window: '1s',
        },
      },
    ],
  },
}
export default config

req

The req argument passed to the function contains the following information:

interface EdgeRequest {
  method: string
  path: string
  queryString: string
  queryParams: Record<string, string[]>
  headers: Record<Lowercase<string>, string[]>
  ip: string
  jwt: Record<string, unknown> | null
  rootFields: { name: string; args: Record<string, unknown>; alias?: string }[]
  operation: 'query' | 'mutation' | 'subscription'
}

rateLimits

Configure the rate limits you want to apply to your GraphQL API. rateLimits can both be a list [...] of rate limit rules or, if you need more flexibility, a callback req => [...] that returns a dynamic list of rate limits, which can be uniquely generated per request.

Independent of function or not, each rule in the list of rate limit rules has the following properties:

Rate Limit Rule

rule.state

Can be enabled, disabled or dryRun.

In dryRun mode, the rate limiting will not block any request if the budget is exceeded. The requests will pass as they would without a rate limit. While in dryRun mode, we will soon show you which requests would’ve been blocked in the Metrics UI.

rule.groupBy

Must be a key that is returned by getConsumerIdentifiers. Every unique consumer identifier will have a separate budget.

rule.group

Unlike groupBy, which refers to a specific consumer identifier returned by getConsumerItenfifiers such as ip or my-identifier, the group property is only available within rate limiting functions and can use a concrete value from the request object:

import { Config } from 'stellate'

const config: Config = {
  config: {
    rateLimits: (req) => [
      {
        name: 'My Rule',
        state: 'enabled',
        group: req.headers['authorization'] ?? req.ip,
        limit: {
          type: 'RequestCount',
          budget: 10,
          window: '1s',
        },
      },
    ],
  },
}
export default config

rule.limit.type

Can be one of two options:

  1. RequestCount: Only allow a certain number of requests to pass. (e.g. 1000 requests per 1 minute)
  2. QueryComplexity: (deprecated) Only allow a certain number of complexity points to pass. (e.g., 1000 query complexity points per 1 minute)
    1. The complexity depends on the query depth and number of attributes. See How to determine the query complexity budget.

rule.limit.window

Time frame for which the expenses of the requests are summed up for. The maximum window is one hour.

Example formats: "1m", "1h", { value: 10, unit: "minute" }

rule.limit.budget

The maximum budget to allow during the window before rate limiting the consumer. This is the “number of request” that match this very rule.

TypeScript Definition

For the TypeScript fans out there, here is the full TypeScript definition of the Rate Limiting API:

export interface RateLimitConfig {
  // getConsumerIdentifiers is optional
  getConsumerIdentifiers?: (
    req: EdgeRequest,
  ) => Record<string, string | undefined>

  // a list of rate limits, can be static, or returned by a callback
  rateLimits?:
    | Array<RateLimitRule | null>
    | ((req: EdgeRequest) => Array<DynamicRateLimitRule | null | undefined>)
}

export interface RateLimitRule {
  name: string
  description?: string
  state?: 'enabled' | 'disabled' | 'dryRun'
  groupBy: GroupBy
  limit: {
    type: 'RequestCount'
    budget: number
    window:
      | `${number}${'s' | 'm' | 'h'}`
      | {
          value: number
          unit: 'seconds' | 'minutes' | 'hours'
        }
  }
}

// When using a callback, rules can either use groupBy or return a group value directly
export type DynamicRateLimitRule = Omit<RateLimitRule, 'groupBy'> &
  (
    | {
        groupBy: GroupBy
      }
    | {
        // Return the actual group value directly (eg. req.ip)
        group: string
      }
  )

// GroupBy allows specifying how requests are bucketed without the need for a getConsumerIdentifiers function
export type GroupBy =
  | 'ip'
  | { header: string }
  | { jwt: string | string[] }
  | { cookie: string }
  | { consumerIdentifier: string }

export interface EdgeRequest {
  method: string
  path: string
  queryString: string
  queryParams: Record<string, string[]>
  headers: Record<Lowercase<string>, string[]>
  ip: string
  jwt: Record<string, unknown> | null
  // Add list of root fields and their arguments (variables should be resolved for arguments)
  rootFields: { name: string; args: Record<string, unknown>; alias?: string }[]
  operation: 'query' | 'mutation' | 'subscription'
}

Examples

We curated a list of real-world examples for you that show-case the power of Stellate's Rate Limiting. You can check them out here: