graphql-limit-count
Limits GraphQL requests by their query cost.
graphql-limit-countPriority 1004What 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
complexityandnode_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 undernode_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.
{
"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.
{
"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.
{
"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>.
{
"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.
{
"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..
{
"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.
{
"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.
{
"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 inX-Ratelimit-Remaining. - Count — Count and Rules cannot be set together; the save is refused.
- Count — A budget of
0is 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 from203.0.113.7leaves 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
ConsumerandAddress, one answer carriesX-Consumer-Ratelimit-LimitandX-Address-Ratelimit-Limit. - Rules — A rule keyed by
$consumer_nameneeds 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_addrwithout 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
complexityand 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 answered403with a Maximum cost of 200. - Maximum cost — Under
node_quantifierwith a ceiling of 500, 600 products with 10 reviews each are answered403. - 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
complexityornode_quantifieronce 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 here
countinteger or stringRequiredThe budget one key may spend in each window: a number, or a $variable read from every request, such as $http_x_quota.
pattern
^([0-9]*[1-9][0-9]*|[$](env|secret)://.+|[$][{][^}]*[}]|[$][A-Za-z_][A-Za-z0-9_.]*)$time_windowinteger or stringRequiredHow long each window lasts: a number, or a $variable read from every request, such as $http_x_window.
pattern
^([0-9]*[1-9][0-9]*|[$](env|secret)://.+|[$][{][^}]*[}]|[$][A-Za-z_][A-Za-z0-9_.]*)$A limit for each key
rulesarray of objectsRequiredrules[].countinteger or stringRequiredWritten as a text it is read at request time as a request variable, or as an environment or secret reference.
pattern
^([0-9]*[1-9][0-9]*|[$](env|secret)://.+|[$][{][^}]*[}]|[$][A-Za-z_][A-Za-z0-9_.]*)$rules[].keystringRequiredRead 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.
pattern
[$]([{][^}]+[}]|[A-Za-z_][A-Za-z0-9_.]*)rules[].time_windowinteger or stringRequiredWritten as a text it is read at request time as a request variable, or as an environment or secret reference.
pattern
^([0-9]*[1-9][0-9]*|[$](env|secret)://.+|[$][{][^}]*[}]|[$][A-Za-z_][A-Za-z0-9_.]*)$rules[].header_prefixstringThe prefix the rate-limit headers of this rule carry.
key_typestringdefault varkeystringdefault remote_addrBehavior
policystringdefault localWhen Policy is redis
redis_databaseintegerdefault 0redis_hoststringRequiredredis_keepalive_poolintegerdefault 100redis_keepalive_timeoutintegerdefault 10000redis_usernamestringredis_passwordstringStored on the engine and shown only to roles that manage this object.
redis_portintegerdefault 6379redis_sslbooleandefault falseredis_ssl_verifybooleandefault falseredis_server_namestringThe 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.
redis_timeoutintegerdefault 1000When Policy is redis-cluster· 8 fields
redis_cluster_namestringRequiredredis_cluster_nodesarray of stringsRequiredredis_cluster_sslbooleandefault falseredis_cluster_ssl_verifybooleandefault falseredis_keepalive_poolintegerdefault 100redis_keepalive_timeoutintegerdefault 10000redis_passwordstringStored on the engine and shown only to roles that manage this object.
redis_timeoutintegerdefault 1000When Policy is redis-sentinel· 13 fields
redis_connect_timeoutintegerdefault 1000redis_databaseintegerdefault 0redis_keepalive_timeoutintegerdefault 60000redis_master_namestringRequiredredis_usernamestringredis_passwordstringStored on the engine and shown only to roles that manage this object.
redis_read_timeoutintegerdefault 1000redis_rolestringdefault masterredis_sentinelsarray of objectsRequiredredis_sentinels[].hoststringRequiredredis_sentinels[].portintegerRequiredsentinel_usernamestringsentinel_passwordstringStored on the engine and shown only to roles that manage this object.
window_typestringdefault fixedrejected_codeintegerdefault 503rejected_msgstringshow_limit_quota_headerbooleandefault trueallow_degradationbooleandefault falsecost_strategystringdefault depthHow a query is priced: by its depth, by weighted complexity, or by the node quantifiers it carries.
When Cost strategy is complexity or node_quantifier
resolve_variablesbooleandefault trueReads query variables and argument defaults into the price instead of treating them as absent.
introspection_endpointstringWhere the schema is read from, on a route whose service carries cost decorations; unset, it is derived from the target.
pattern
^https?://introspection_headersobjectHeaders 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.
max_costnumberdefault 0A query priced above this is refused; its cost is still charged to the quota first. Zero turns the check off.
score_factornumberdefault 1Scales the raw cost before it is charged against the quota.
Advanced
groupstringsync_intervalnumberdefault -1Environment-wide settings
Set once per environment, under Plugins › graphql-limit-count. Every graphql-limit-count in the environment uses these.
limit_headerstringdefault X-RateLimit-LimitName of the response header that carries the quota.
remaining_headerstringdefault X-RateLimit-RemainingName of the response header that carries what is left of the quota.
reset_headerstringdefault X-RateLimit-ResetName of the response header that carries the seconds until the window resets.