Skip to main content

graphql-limit-count

Limits GraphQL requests by their query cost.

Category TrafficAdd it to Route, Service, Consumer, Consumer group, Global rule, Plugin configEngine plugin graphql-limit-countPriority 1004

What it does​

graphql-limit-count gives each key a budget for the GraphQL queries that reach the route or other object it is added to. Each query is charged its price rather than one. Cost strategy sets the price, the depth of the query unless you choose another, so { orders { id } } costs 2. While a key stays within Count for each Time window, its queries pass and every answer carries X-Ratelimit-Limit and X-Ratelimit-Remaining.

A key over its budget is answered with Rejected code, 503 unless you set another. A query priced above Maximum cost is answered 403 on its own, and its price is still charged to the budget first.

How a query is priced​

Each strategy reads the query the client sends in the body of its POST and charges the result, scaled by Score factor.

  • With depth, the default, a query costs how deep it reaches: { orders { id } } costs 2.
  • With complexity, every field the query selects adds to its price, and an order query that asks for three of its fields costs 6.
  • With node_quantifier, the page size a list asks for multiplies only what is nested under that list. 50 products with 10 reviews each cost 52, while 5000 products with nothing listed under them cost 2.
  • Under complexity and node_quantifier, GraphQL cost decorations on the route's service weight the fields they name, and the schema is read from the route's upstream. Without them, every query costs 1 under node_quantifier.

When to use it​

  • A GraphQL orders API where one deep query costs the backend as much as many simple ones, and each client address gets a budget of 100 depth units a minute.
  • Partners on a GraphQL endpoint, each consumer with a budget per minute and each client address a larger one per hour, under Rules.
  • Queries that fetch lists inside lists, priced by the page sizes they ask for, with a ceiling under Maximum cost that refuses the largest outright.
  • Several gateway nodes that keep one budget per client between them, in one Redis server, a Redis cluster or behind Redis Sentinel.

Not for counting requests one by one, whatever they ask for — see limit-count (Limits the number of requests in a time window, per key).

Set it up​

Charge each query by how many times its nested lists are fetched​

The route must belong to a service that carries cost decorations naming the list arguments, such as the cost decoration example "Charge a product list by the page size it asks for"; without them every query costs 1. A list's page size multiplies only what is nested under it, so a flat list of any size costs almost nothing; to charge a page size itself use the complexity strategy. The schema is read from the route's upstream.

Under Basics choose One limit for everything here, set Count to 5000 and set Time window to 60 seconds. Under Behavior set Cost strategy to node_quantifier, turn on Resolve variables and set Maximum cost to 500.

graphql-limit-count · as the console saves it
{
"count": 5000,
"time_window": 60,
"cost_strategy": "node_quantifier",
"max_cost": 500,
"resolve_variables": true
}

More scenarios​

Charge each query by its complexity and refuse any over the cost ceiling

Without cost decorations on the route's service every field costs one; with them, the decorated fields are weighted and the schema is read from the route's upstream.

Under Basics choose One limit for everything here, set Count to 1000 and set Time window to 60 seconds. Under Behavior set Cost strategy to complexity and set Maximum cost to 200.

graphql-limit-count · as the console saves it
{
"count": 1000,
"time_window": 60,
"cost_strategy": "complexity",
"max_cost": 200
}
Share the counters through a Redis master that Sentinel keeps track of

Under Basics choose One limit for everything here, set Count to 100 and set Time window to 60 seconds. Under Behavior set Policy to redis-sentinel, set Redis master name to ratelimit, set Redis password to <secret> and fill Redis sentinels as in the JSON below.

graphql-limit-count · as the console saves it
{
"count": 100,
"time_window": 60,
"policy": "redis-sentinel",
"redis_master_name": "ratelimit",
"redis_sentinels": [
{
"host": "10.0.7.21",
"port": 26379
},
{
"host": "10.0.7.22",
"port": 26379
}
],
"redis_password": "<secret>"
}
Share the counters between gateway nodes through a Redis cluster

Under Basics choose One limit for everything here, set Count to 100 and set Time window to 60 seconds. Under Behavior set Policy to redis-cluster, set Redis cluster name to ratelimit, fill Redis cluster nodes as in the JSON below and set Redis password to <secret>.

graphql-limit-count · as the console saves it
{
"count": 100,
"time_window": 60,
"policy": "redis-cluster",
"redis_cluster_name": "ratelimit",
"redis_cluster_nodes": [
"10.0.7.11:6379",
"10.0.7.12:6379",
"10.0.7.13:6379"
],
"redis_password": "<secret>"
}
Share the counters between gateway nodes through one Redis server

Under Basics choose One limit for everything here, set Count to 100 and set Time window to 60 seconds. Under Behavior set Policy to redis, set Redis host to 10.0.7.5, set Redis password to <secret> and leave Redis port at 6379.

graphql-limit-count · as the console saves it
{
"count": 100,
"time_window": 60,
"policy": "redis",
"redis_host": "10.0.7.5",
"redis_port": 6379,
"redis_password": "<secret>"
}
Give each client address a budget of 100 depth units a minute

Every request is a POST whose JSON body carries the query, and each one spends its depth from the budget, so { orders { id } } spends 2. Behind a load balancer the address is the balancer's unless real-ip is set up, after which each forwarded address has its own budget.

Under Basics choose One limit for everything here, set Count to 100 and set Time window to 60 seconds. Under Behavior set Rejected code to 429 and set Rejected message to Too many GraphQL requests, slow down..

graphql-limit-count · as the console saves it
{
"count": 100,
"time_window": 60,
"rejected_code": 429,
"rejected_msg": "Too many GraphQL requests, slow down."
}
Give each consumer a limit per minute and each client address a limit per hour

Needs an authentication plugin on the same route, which is what names the consumer; the quota headers carry each rule's prefix after X-.

Under Basics choose A limit for each key and fill Rules as in the JSON below.

graphql-limit-count · as the console saves it
{
"rules": [
{
"key": "$consumer_name",
"count": 100,
"time_window": 60,
"header_prefix": "Consumer"
},
{
"key": "$remote_addr",
"count": 1000,
"time_window": 3600,
"header_prefix": "Address"
}
]
}
Rename the quota headers every route that carries the plugin sends backenvironment-wide settings

Set Limit header to X-GraphQL-Limit, set Remaining header to X-GraphQL-Remaining and set Reset header to X-GraphQL-Reset.

graphql-limit-count environment-wide settings · as the console saves it
{
"limit_header": "X-GraphQL-Limit",
"remaining_header": "X-GraphQL-Remaining",
"reset_header": "X-GraphQL-Reset"
}

Things to know​

  • Count — Only a POST carrying a GraphQL query reaches the counter, anything else is refused with 405 or 400, and a request costs its query’s price under the cost strategy, scaled by the score factor, rather than one.
  • Count — Each query spends its price from the budget. With a budget of 100 and the default depth pricing, two queries of { orders { id } } leave 96 in X-Ratelimit-Remaining.
  • Count — Count and Rules cannot be set together; the save is refused.
  • Count — A budget of 0 is refused when you save.
  • Time window — Text is read on each request as a $variable, ${name}, $env:// or $secret:// reference. Any other text is saved and then fails every request with 500.
  • Key — The client address is the address a request comes from. With real-ip on the same route reading X-Forwarded-For, each forwarded address has a budget of its own, and a first query from 203.0.113.7 leaves 98.
  • Key — A variable the request does not carry, or a name with a dollar sign under Variable, is saved without a word, and every request is then counted by client address. So is a combination in which no $name resolves.
  • Rules — Each rule's quota headers carry its Header prefix. With Consumer and Address, one answer carries X-Consumer-Ratelimit-Limit and X-Address-Ratelimit-Limit.
  • Rules — A rule keyed by $consumer_name needs an authentication plugin on the same route, for example key-auth, which is what names the consumer.
  • Rules — An empty list of rules is refused when you save.
  • Rules — Two rules with the same Key are refused when you save.
  • Key — A rule's key names at least one $variable. remote_addr without its dollar sign is refused when you save.
  • Allow degradation — When the limit cannot be counted, because the store is unreachable or a value resolves to nothing, off answers 500 and on lets every request through unlimited.
  • Cost strategy — With complexity and no cost decorations, an order query that asks for three of its fields costs 6, and a second one leaves 988 of a budget of 1000.
  • Cost strategy — With node_quantifier, a list's page size multiplies only what is nested under it. 50 products with 10 reviews each cost 52, and 5000 products with nothing listed under them cost 2.
  • Maximum cost — Under complexity, a query of 200 fields is answered 403 with a Maximum cost of 200.
  • Maximum cost — Under node_quantifier with a ceiling of 500, 600 products with 10 reviews each are answered 403.
  • Group — Group cannot be set beside Rules; the save is refused.
  • Sync interval — With a Redis policy, Sync interval must be smaller than Time window, or the save is refused.
  • Header names in the plugin metadata apply to every route that carries the plugin. With Limit header at X-GraphQL-Limit, the budget of 100 comes back in that header.
  • AdviceStart with the default depth pricing, and move to complexity or node_quantifier once cost decorations describe the fields that cost your backend the most.

Settings​

The fields as the console shows them, section by section. A field under “When … is …” is there only while that value is set.

Basics​

Choose one:

  • One limit for everything hereA count and a window, counted under the key below for every query this object serves.
  • A limit for each keyA list of rules, each counting the key it names with a count and a window of its own.

One limit for everything here​

Countcountinteger or stringRequired

The budget one key may spend in each window: a number, or a $variable read from every request, such as $http_x_quota.

integer over 0 · a whole number above zero, a $variable, or a $secret:// or $env:// reference
pattern^([0-9]*[1-9][0-9]*|[$](env|secret)://.+|[$][{][^}]*[}]|[$][A-Za-z_][A-Za-z0-9_.]*)$
Time windowtime_windowinteger or stringRequired

How long each window lasts: a number, or a $variable read from every request, such as $http_x_window.

integer over 0 · in seconds · a whole number above zero, a $variable, or a $secret:// or $env:// reference
pattern^([0-9]*[1-9][0-9]*|[$](env|secret)://.+|[$][{][^}]*[}]|[$][A-Za-z_][A-Za-z0-9_.]*)$

A limit for each key​

Rulesrulesarray of objectsRequired
at least 1 item
Each item:
Countrules[].countinteger or stringRequired

Written as a text it is read at request time as a request variable, or as an environment or secret reference.

integer over 0 · a whole number above zero, a $variable, or a $secret:// or $env:// reference
pattern^([0-9]*[1-9][0-9]*|[$](env|secret)://.+|[$][{][^}]*[}]|[$][A-Za-z_][A-Za-z0-9_.]*)$
Keyrules[].keystringRequired

Read at request time as a template: a key that names no $variable drops its rule, and a request left with no rule at all fails.

a $variable, with or without braces
pattern[$]([{][^}]+[}]|[A-Za-z_][A-Za-z0-9_.]*)
Time windowrules[].time_windowinteger or stringRequired

Written as a text it is read at request time as a request variable, or as an environment or secret reference.

integer over 0 · in seconds · a whole number above zero, a $variable, or a $secret:// or $env:// reference
pattern^([0-9]*[1-9][0-9]*|[$](env|secret)://.+|[$][{][^}]*[}]|[$][A-Za-z_][A-Za-z0-9_.]*)$
Header prefixrules[].header_prefixstring

The prefix the rate-limit headers of this rule carry.

Key typekey_typestringdefault var
one of constant, var, var_combination
Keykeystringdefault remote_addr

Behavior​

Policypolicystringdefault local
one of local, redis, redis-cluster, redis-sentinel

When Policy is redis​

Redis databaseredis_databaseintegerdefault 0
at least 0
Redis hostredis_hoststringRequired
at least 2 characters
Redis keepalive poolredis_keepalive_poolintegerdefault 100
at least 1
Redis keepalive timeoutredis_keepalive_timeoutintegerdefault 10000
at least 1000 · in milliseconds
Redis usernameredis_usernamestring
at least 1 character
Redis passwordredis_passwordstring

Stored on the engine and shown only to roles that manage this object.

Redis portredis_portintegerdefault 6379
at least 1
Redis SSLredis_sslbooleandefault false
Redis SSL verifyredis_ssl_verifybooleandefault false
Server name for TLSredis_server_namestring

The server name sent in the TLS handshake and, with verification on, checked against the certificate; unset, the host is sent, and an address never is.

at least 1 character
Redis timeoutredis_timeoutintegerdefault 1000
at least 1 · in milliseconds
When Policy is redis-cluster· 8 fields
Redis cluster nameredis_cluster_namestringRequired
Redis cluster nodesredis_cluster_nodesarray of stringsRequired
at least 1 item; each 2 to 100 characters
Redis cluster SSLredis_cluster_sslbooleandefault false
Redis cluster SSL verifyredis_cluster_ssl_verifybooleandefault false
Redis keepalive poolredis_keepalive_poolintegerdefault 100
at least 1
Redis keepalive timeoutredis_keepalive_timeoutintegerdefault 10000
at least 1000 · in milliseconds
Redis passwordredis_passwordstring

Stored on the engine and shown only to roles that manage this object.

Redis timeoutredis_timeoutintegerdefault 1000
at least 1 · in milliseconds
When Policy is redis-sentinel· 13 fields
Redis connect timeoutredis_connect_timeoutintegerdefault 1000
at least 1 · in milliseconds
Redis databaseredis_databaseintegerdefault 0
at least 0
Redis keepalive timeoutredis_keepalive_timeoutintegerdefault 60000
at least 1 · in milliseconds
Redis master nameredis_master_namestringRequired
at least 1 character
Redis usernameredis_usernamestring
at least 1 character
Redis passwordredis_passwordstring

Stored on the engine and shown only to roles that manage this object.

Redis read timeoutredis_read_timeoutintegerdefault 1000
at least 1 · in milliseconds
Redis roleredis_rolestringdefault master
one of master, slave
Redis sentinelsredis_sentinelsarray of objectsRequired
at least 1 item
Each item:
Hostredis_sentinels[].hoststringRequired
at least 2 characters
Portredis_sentinels[].portintegerRequired
1 to 65535
Sentinel usernamesentinel_usernamestring
at least 1 character
Sentinel passwordsentinel_passwordstring

Stored on the engine and shown only to roles that manage this object.

Window typewindow_typestringdefault fixed
one of fixed, sliding
Rejected coderejected_codeintegerdefault 503
200 to 599
Rejected messagerejected_msgstring
at least 1 character
Show limit quota headershow_limit_quota_headerbooleandefault true
Allow degradationallow_degradationbooleandefault false
Cost strategycost_strategystringdefault depth

How a query is priced: by its depth, by weighted complexity, or by the node quantifiers it carries.

one of complexity, depth, node_quantifier

When Cost strategy is complexity or node_quantifier​

Resolve variablesresolve_variablesbooleandefault true

Reads query variables and argument defaults into the price instead of treating them as absent.

Introspection endpointintrospection_endpointstring

Where the schema is read from, on a route whose service carries cost decorations; unset, it is derived from the target.

an http or https address
pattern^https?://
Introspection headersintrospection_headersobject

Headers sent with the schema introspection request, for a target that asks for credentials. Stored on the engine and shown only to roles that manage this object. An object whose keys are a name without a colon, each with a string as its value.

Maximum costmax_costnumberdefault 0

A query priced above this is refused; its cost is still charged to the quota first. Zero turns the check off.

at least 0
Score factorscore_factornumberdefault 1

Scales the raw cost before it is charged against the quota.

over 0

Advanced​

Groupgroupstring
Sync intervalsync_intervalnumberdefault -1
in seconds

Environment-wide settings​

Set once per environment, under Plugins › graphql-limit-count. Every graphql-limit-count in the environment uses these.

Limit headerlimit_headerstringdefault X-RateLimit-Limit

Name of the response header that carries the quota.

Remaining headerremaining_headerstringdefault X-RateLimit-Remaining

Name of the response header that carries what is left of the quota.

Reset headerreset_headerstringdefault X-RateLimit-Reset

Name of the response header that carries the seconds until the window resets.

Every plugin also has Enabled, Priority, Filter, Error response and Pre-function: see Plugin execution settings.

G8 dev · Apache APISIX® 3.19.0
Apache, Apache APISIX and APISIX are trademarks of The Apache Software Foundation.
Facts
Category
Traffic
Add it to
Route, Service, Consumer, Consumer group, Global rule, Plugin config
Engine plugin
graphql-limit-count
Priority
1004