Jan 24

Announcing GraphQL Rate Limiting: Protect Your GraphQL API from Bots and Hackers

Today, we’re excited to announce the public beta of our brand-new GraphQL Rate Limiting! Sign up now or check out the docs and start using it today to protect your GraphQL API.

Through our conversations with hundreds of companies using GraphQL in production, we’ve learned about some common pain points:

  1. Bots spamming their addToCart mutation whenever they drop new limited edition products

  2. Hackers sending lots of requests to the login mutation, trying to crack their users’ passwords

  3. Consumers exceeding SLAs they’ve signed that limit the number of times they can request certain data

The solution for all of these is to rate limit the number of calls any single consumer can make. For example, only allowing two login or addToCart mutations from a single actor every ten seconds. However, with GraphQL APIs, all requests go to a single path like /graphql, so HTTP rate limiting tooling that limits number-of-calls-per-path doesn’t work for them!

So, how is Stellate's GraphQL Rate Limiting different?

Rate Limiting Specific GraphQL Operations

This is where our GraphQL Rate Limiting is different. It’s built upon our deep understanding of GraphQL, and going back to our idea of universal configuration as code, the configuration is as flexible as your needs.

For example, to rate limit the addToCart mutation to 2 operations per 10s per IP, you would do:

import { Config } from 'stellate'
import { byField } from 'stellate/rate-limiting'
const config: Config = {
config: {
// Allow 2 addToCart mutation calls per 10s per IP
rateLimits: (req) => byField(req, {
name: 'Limit addToCart mutations',
groupBy: 'ip',
mutationFields: {
addToCart: {
budget: 2,
window: '10s',
},
},
}),
},
}
export default config

We allow setting up to 20 limits per request and identifying consumers by any part of the HTTP request. You can for example set different limits for authenticated vs. unauthenticated users:

const config: Config = {
config: {
rateLimits: (req) => {
if (req.jwt?.sub) {
// Allow 50 req / min from authenticated users
return [
{
name: 'Rate Limit for authenticated users (by JWT sub)',
group: req.jwt.sub, // Group by the JWT subject claim (<https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2>)
limit: {
type: 'RequestCount',
budget: 50,
window: '1m',
},
},
]
}
// Allow 10 req / min per IP from unauthenticated users
return [
{
name: 'Rate Limit for unautheticated users (by IP)',
groupBy: 'ip',
limit: {
type: 'RequestCount',
budget: 10,
window: '1m',
},
},
]
},
},
}

This flexibility means the possibilities are endless and you can rate limit your GraphQL API exactly how you need to!

Sign up and start using Stellate’s GraphQL Rate Limiting today →

What does “Public Beta” mean?

Our Rate Limiting is now available for all users of the Stellate platform, new and old. Rate limiting is free during the public beta as part of the general $10 / 1M request pricing for Stellate, but we will add standalone pricing for the rate limiting once we move into GA. (if you have opinions on what you would like this to look like, please reach out!)

Because this is still an early product, it is possible that the rate limiting will incorrectly let some requests pass through that should’ve been blocked. However, we already guarantee that enabling rate limiting will not break your requests: if something is wrong with the rate limiting, we will fail open and your users won’t be affected.

For now, we don’t support persisted queries or batched requests. If rate limiting is enabled for your GraphQL API and a request with either of these is sent, we will automatically block the request.

For extra caution, we have a dryRun mode that won’t block any requests but will highlight how many requests and consumers a particular rule would have blocked if it was not in dryRun mode:

Coming Soon: Complexity-based Rate Limiting

Beyond the operation-specific limits, there are some common abuse vectors with GraphQL that malicious actors can abuse to put high load on your infrastructure, including batching, aliasing, and deep nesting.

Limiting the size and depth of the operations, the amount of aliases as well as the pagination amounts your API accepts is a good start. However, that doesn’t prevent malicious actors from hammering your infrastructure with lots of medium-expensive operations that look similar to the ones you send from your clients:

# This query fetches up to 63.125 nodes despite looking relatively innocuous:
# it's neither deeply nested, nor batched, nor does it use aliases or high
# pagination counts.
query evilQuery {
threads(first: 25) {
messages(first: 25) { ... }
participants(first: 25) {
threads(first: 25) { ... }
communities(first: 25) { ... }
channels(first: 25) { ... }
feed(first: 25) { ... }
}
}
}

The only way to prevent these attacks is to use persisted queries (which you can only do if you control all the clients) or by rate limiting the complexity count of the operations a single consumer sends.

Spoiler alert: our GraphQL Rate Limiting will soon support the latter out of the box!

You’ll be able to rate limit complexity points per time window, just like you can request counts above:

import { Config } from 'stellate'
const config: Config = {
config: {
rateLimits: [
// Allow 1000 complexity points / min from a single IP
{
name: 'IP Limit',
groupBy: 'ip',
limit: {
type: 'QueryComplexity',
budget: 1000,
window: '1m',
},
}),
],
},
}
export default config

If you would like early access to complexity-based rate limiting, please upvote the feature request: https://stellate.canny.io/feature-requests/p/complexity-based-rate-limiting