Skip to main content
Version: development

GraphQL Query Rate Limiting

Overview

This policy is an example of how to implement rate limiting for GraphQL queries using the Classifier.

Configuration

This policy is based on the Rate Limiting blueprint. It contains a Classifier that extracts the userID claim from a JWT token in the request's authorization header and then rate limit unique users based on the extracted user_id Flow Label; todo-service.prod.svc.cluster.local is selected as the target service for this policy.

tip

Classification rules can be written based on HTTP requests, and scheduler priorities can be defined based on Flow Labels by live previewing them first using introspection APIs.

The below values.yaml file can be generated by following the steps in the Installation section.

# yaml-language-server: $schema=../../../../../../blueprints/rate-limiting/base/gen/definitions.json
blueprint: rate-limiting/base
uri: ../../../../../../../blueprints
policy:
policy_name: "graphql-rate-limiting"
rate_limiter:
bucket_capacity: 40
fill_amount: 2
selectors:
- control_point: ingress
service: todo-service.svc.cluster.local
parameters:
limit_by_label_key: user_id
interval: 1s
resources:
flow_control:
classifiers:
- selectors:
- control_point: ingress
service: todo-service.svc.cluster.local
rego:
labels:
user_id:
telemetry: true
module: |
package graphql_example
import future.keywords.if
query_ast := graphql.parse_query(input.parsed_body.query)
claims := payload if {
io.jwt.verify_hs256(bearer_token, "secret")
[_, payload, _] := io.jwt.decode(bearer_token)
}
bearer_token := t if {
v := input.attributes.request.http.headers.authorization
startswith(v, "Bearer ")
t := substring(v, count("Bearer "), -1)
}
queryIsCreateTodo if {
some operation
walk(query_ast, [_, operation])
operation.Name == "createTodo"
count(operation.SelectionSet) > 0
some selection
walk(operation.SelectionSet, [_, selection])
selection.Name == "createTodo"
}
user_id := u if {
queryIsCreateTodo
u := claims.userID
}

Generated Policy

apiVersion: fluxninja.com/v1alpha1
kind: Policy
metadata:
labels:
fluxninja.com/validate: "true"
name: graphql-rate-limiting
spec:
circuit:
components:
- flow_control:
rate_limiter:
in_ports:
bucket_capacity:
constant_signal:
value: 40
fill_amount:
constant_signal:
value: 2
out_ports:
accept_percentage:
signal_name: ACCEPT_PERCENTAGE
parameters:
interval: 1s
limit_by_label_key: user_id
request_parameters: {}
selectors:
- control_point: ingress
service: todo-service.svc.cluster.local
- decider:
in_ports:
lhs:
signal_name: ACCEPT_PERCENTAGE
rhs:
constant_signal:
value: 90
operator: gte
out_ports:
output:
signal_name: ACCEPT_PERCENTAGE_ALERT
- alerter:
in_ports:
signal:
signal_name: ACCEPT_PERCENTAGE_ALERT
parameters:
alert_name: More than 90% of requests are being rate limited
evaluation_interval: 1s
resources:
flow_control:
classifiers:
- rego:
labels:
user_id:
telemetry: true
module: |
package graphql_example
import future.keywords.if
query_ast := graphql.parse_query(input.parsed_body.query)
claims := payload if {
io.jwt.verify_hs256(bearer_token, "secret")
[_, payload, _] := io.jwt.decode(bearer_token)
}
bearer_token := t if {
v := input.attributes.request.http.headers.authorization
startswith(v, "Bearer ")
t := substring(v, count("Bearer "), -1)
}
queryIsCreateTodo if {
some operation
walk(query_ast, [_, operation])
operation.Name == "createTodo"
count(operation.SelectionSet) > 0
some selection
walk(operation.SelectionSet, [_, selection])
selection.Name == "createTodo"
}
user_id := u if {
queryIsCreateTodo
u := claims.userID
}
selectors:
- control_point: ingress
service: todo-service.svc.cluster.local

info

Circuit Diagram for this policy.

For example, if the mutation query is as follows

mutation createTodo {
createTodo(input: { text: "todo" }) {
user {
id
}
text
done
}
}

Without diving deep into how Rego works, the source section mentioned in this use-case does the following:

  1. Parse the query
  2. Check if the mutation query is createTodo
  3. Verify the JWT token with a secret key secret (only for demonstration purposes)
  4. Decode the JWT token and extract the userID from the claims
  5. Assign the value of userID to the exported variable userID in Rego source

From there on, the Classifier rule assigns the value of the exported variable userID in Rego source to user_id flow label, effectively creating a label user_id:1. This label is used by the Rate Limiter component in the policy to limit the createTodo mutation query to 2 requests per second, with a burst capacity of 40 requests for each userID.

Installation

Generate a values file specific to the policy. This can be achieved using the command provided below.

aperturectl blueprints values --name=rate-limiting/base --version=main --output-file=values.yaml

Apply the policy using the aperturectl CLI or kubectl.

aperturectl cloud blueprints apply --values-file=values.yaml

Policy in Action

In this example, the traffic generator is configured to generate 50 requests per second for 2-minutes. When loading the above policy in the playground, you can observe that it accepts no more than 2 requests per second at any given time, and rejects the rest of the requests.

GraphQL Status Rate Limiting