Skip to content

Rate Limiting

Erik Roberts edited this page Feb 2, 2024 · 1 revision

An important part of any API is making sure that users are not able to bombard the server with requests, effectively initiating a denial-of-service attack by hogging server resources. In a typical RESTful API, this is usually accomplished via rate limiting: the user is only permitted to send a certain number of requests within an allotted amount of time. In GraphQL APIs, this is not as effective, since users are able to request as much data as they'd like within a single GraphQL request. Therefore, in the GraphQL world, rate limiting is usually accompanied by query complexity analysis.

ℹ️ NestJS has documentation on complexity analysis available here.

Basics

When a GraphQL request comes in, we're able to analyze the request AST to see what properties have been requested, and therefore, what resolvers will be called. Each resolver can then be assigned a complexity value. The complexity values of all requested resolvers are added up, and if they are above the set complexity limit, the request is denied before any resolvers have actually been called.

ℹ️ There are multiple ways you could add pre-resolver calculations like this, however we use an Apollo plugin.

Functions and Estimators

A lot of the time your resolvers are not going to have a static complexity. Instead, the complexity is going to vary based on the arguments passed in by the user. For example, a request with a Pagination input asking for 500 items is going to use more bandwidth than a request asking for only five items. Similarly, a request sorting by a field that is indexed in your database is going to use less compute power than a request sorting by a field that is indexed. Instead of assigning a static complexity value to your resolver, you can assign an estimator function instead. The estimator can use the resolver arguments to calculate a complexity value.

ℹ️ A perfect complexity estimator would directly correlate with the amount of compute power and bandwidth used; however, this can be unrealistic implementation-wise.

Glimpse comes with a number of built-in complexity estimators available within /src/gql/gql-complexity.plugin.ts. There is a built-in estimator available for every RuleType. These complexity estimators are applied within the GraphQL decorator's options on the resolver. For example:

@Mutation(() => Vote, { complexity: Complexities.Update })

If you want to provide your own estimator implementation, you can do so:

function customEstimator(options: ComplexityEstimatorArgs): number {
    return 10 + options.childComplexity;
}

The ComplexityEstimatorArgs definition is available in the graphql-query-complexity package respository:

export type ComplexityEstimatorArgs = {
  type: GraphQLCompositeType;
  field: GraphQLField<any, any>;
  node: FieldNode;
  args: { [key: string]: any };
  childComplexity: number;
  context?: Record<string, any>;
};

Credits

We can combine the concepts of rate limiting and query complexities to make sure our users are not overloading the API with requests. One way to do this would be to simply implement both methods independently: for example, users may only send 60 requests a minute and each one has a complexity cap of 500. However, a better solution would arguably be to have a stateful complexity cap for each user which persists across requests and is periodically reset.

To match the previous example, we could have a a query complexity cap of 30,000 (60 * 500) and then reset that cap every 60 seconds. This would let users spend their 30,000 "credits" however they want within the 60 second interval, and then after 60 seconds, their credits are reset back to 30,000. Users could choose to spend all 30,000 credits in a single request, or they could spend 30 credits across 1,000 different requests.

⚠️ Credits in this context are not to be confused with the database type "Credit".

The number of credits each user has can be determined based on a number of factors: their originating IP, whether they're logged in, what group(s) they're in, etc. Regardless, your average user on the UI should not run into rate limits if they are using the application in a typical way. Therefore, coordination is required with those working on the UI when determining the maximum number of credits.

This is all still an open issue.