Practical Strategies for GraphQL API Rate Limiting
Implementing rate limiting on REST APIs is relatively straightforward, as we commonly use URI paths to represent specific API resources and HTTP methods to indicate operations on resources. The proxy layer can easily enforce pre-defined rate-limiting rules based on this information.
However, the scenario becomes significantly more complex when it comes to GraphQL APIs. Let’s delve into how to overcome these challenges.
Simple Scenarios
In contrast to REST APIs, GraphQL utilizes a proprietary query language. It no longer relies on paths and HTTP methods for resource retrieval and manipulation but unifies data querying and operations under Query and Mutation, where Query fetches data, and mutation performs data manipulations such as create, update, and delete.
GET /users
GET /users/1
POST /users
PUT /users/1
DELETE /users/1
query { users { fullName } }
query { user(id: 1) { fullName } }
mutation { createUser(user: { lastName: "Jack" }) { id, fullName } }
mutation { updateUser (id: 1, update: { lastName: "Marry" }) { fullName } }
mutation { deleteUser (id: 1) }
The above examples highlight the shift in API query methods. Unlike REST, GraphQL resembles calling functions on resources, passing necessary input parameters, with the response containing the queried data. Besides differences in query methods, GraphQL exposes APIs typically through a single endpoint (e.g., /graphql), with queries and input parameters sent via the POST body.
Consider the following example:
query {
users {
fullName
}
photos {
url
uploadAt
}
}
In this scenario, simulating the homepage of an album, an API call to the /graphql
endpoint queries both user and photo lists simultaneously. Now, contemplate whether traditional reverse proxy strategies, executed at the request level, remain applicable to GraphQL APIs.
The answer is no. Traditional reverse proxy servers cannot effectively handle GraphQL API calls containing the queries themselves, making it impossible to enforce policies like rate limiting. For GraphQL APIs, the granularity of “HTTP requests” appears too coarse.
However, the API gateway, Apache APISIX incorporates built-in support for GraphQL to HTTP capabilities. Administrators can preconfigure a query statement, allowing clients to call it directly through an HTTP POST without understanding GraphQL details, only providing necessary input parameters. This not only enhances security but also enables the application of HTTP API policies in this context.
Effectively, this transforms dynamic GraphQL queries into knowledge-driven static queries provided by API providers, presenting both advantages and disadvantages. At times, we might not wish to sacrifice the dynamic querying feature of GraphQL. Let’s continue the discussion of other scenarios.
Complex Scenarios
GraphQL utilizes its specialized language for data modeling and API descriptions, allowing nested data structures. Expanding on the previous example:
query {
photos(first: 10) {
url
uploadAt
publisher {
fullName
avatar
}
comments(first: 10) {
content
sender {
fullName
avatar
}
}
}
// users...
}
In this case, simulating the retrieval of the first 10 photos, including the publisher for each photo and the first 10 comments with their senders, the backend service must handle queries involving multiple database tables or calls to microservices. In such nested query scenarios, as data quantity and nesting levels increase, computational stress on backend services and databases rises exponentially.
To prevent complex queries from overwhelming the service, we may want to inspect and block such queries at the proxy layer. To apply this strategy, the proxy component must parse query statements into structured data, traverse them to obtain nested fields in each layer, and follow GraphQL’s common practice of assigning complexity values to fields as query costs. Global limits on the overall query complexity can then be enforced. For the above query, assuming a cost of 1 for each individual field:
10 * photo (url + uploadAt + publisher.fullName + publisher.avatar + 10 * comment (content + sender.fullName + sender.avatar))
10 * (1 + 1 + 1 + 1 + 10 * (1 + 1 + 1)) = 340
With a total query cost of 340, it seems acceptable, and we can configure API query cost limits based on such rules. However, if a malicious client attempts to fetch data for 100 photos in a single query, the query cost will soar to 3400, surpassing the predefined limit, and resulting in a denied request.
Beyond restricting the maximum cost per client query, additional limits on time intervals, such as allowing clients a total of 2000 queries per minute and rejecting excess queries, can thwart malicious crawlers.
To implement such capabilities, the proxy component must parse and calculate query costs. API7 Enterprise supports these features, enabling dynamic parsing of GraphQL queries and implementing user rate limits based on configurations.
GraphQL APIs face challenges at the proxy layer, where traditional reverse proxies struggle to effectively perceive and handle the complexity and nesting relationships within GraphQL query statements. In contrast, API gateway technologies prove invaluable in overcoming these challenges where API7 Enterprise can be a great choice.