Rate Limiting in GraphQL

Introduction

In this lesson, we'll focus on securing your GraphQL API by implementing rate limiting. As we secure our APIs, it's crucial to prevent abuse, and rate limiting is a powerful tool for this purpose. Rate limiting helps manage the number of requests a user can make to your API within a specific time frame, ensuring it can handle heavy loads gracefully.

We'll use the rack-attack gem in this lesson. This gem is popular for rate limiting in Rack-based applications (including Sinatra) due to its simplicity and flexibility.

By the end of this lesson, you'll be equipped to add rate limiting to your GraphQL API, protecting your resources and improving the performance and reliability of your server.

Defining the GraphQL Schema

The schema defines the structure of the data and the queries that can be made. We'll define a simple schema for our books example using graphql-ruby's class-based approach:

require 'graphql'

class BookType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :title, String, null: false
  field :author, String, null: false
end

class QueryType < GraphQL::Schema::Object
  field :books, [BookType], null: false

  def books
    [
      { id: '1', title: 'The Hobbit', author: 'J.R.R. Tolkien' },
      { id: '2', title: 'Harry Potter', author: 'J.K. Rowling' }
    ]
  end
end

class AppSchema < GraphQL::Schema
  query QueryType
end

Here's a brief explanation:

  • BookType: Defines a book type with fields id, title, and author, all of which are non-nullable.
  • QueryType: Defines a query to fetch a list of books with a resolver method that returns sample data.
  • AppSchema: The main schema class that ties everything together.

Implementing Rate Limiting in Sinatra

Now, let's introduce rate limiting to our Sinatra application using Rack middleware. We'll use the rack-attack gem for this. Here's how to set it up:

First, require the necessary libraries:

require 'sinatra'
require 'rack/attack'

Next, configure Rack::Attack with rate limiting settings:

use Rack::Attack

Rack::Attack.cache.store = Rack::Attack::StoreProxy::MemoryStoreProxy.new

Rack::Attack.throttle('req/ip', limit: 100, period: 15 * 60) do |req| # period in seconds (15 minutes = 900 seconds)
  req.ip
end

Rack::Attack.throttled_responder = lambda do |env|
  [429, { 'Content-Type' => 'text/plain' }, ['Too many requests']]
end

Here's a breakdown of this code:

  • cache.store: Sets up an in-memory store for tracking requests (you can use Redis in production).
  • throttle: Defines the rate limit rule — 100 requests per 15 minutes per IP address.
  • req.ip: The discriminator that identifies unique clients (by IP address).
  • throttled_responder: Custom response when the rate limit is exceeded (429 status code).

To apply rate limiting to specific endpoints only, you can add conditions within the throttle block, such as checking req.path.

Integrating graphql-ruby with Sinatra

Next, we need to set up our GraphQL endpoint in Sinatra. We'll create a POST route that handles GraphQL queries:

require 'json'

post '/graphql' do
  request_payload = JSON.parse(request.body.read)
  query = request_payload['query']
  variables = request_payload['variables'] || {}
  
  result = AppSchema.execute(
    query,
    variables: variables,
    context: {}
  )
  
  content_type :json
  result.to_json
end

In this setup:

  • We parse the incoming JSON request body to extract the GraphQL query and variables.
  • AppSchema.execute: Executes the GraphQL query against our schema.
  • The result is converted to JSON and returned to the client.

Finally, start the Sinatra server:

set :port, 4000

if __FILE__ == $0
  puts "🚀 Server ready at http://localhost:4000/graphql"
  Sinatra::Application.run!
end

Testing the Implementation

To test our implementation, we'll query the GraphQL API to see if rate limiting is working as intended. Here's a Ruby script using net/http to send 105 requests — just enough to exceed the limit of 100 requests per 15 minutes and trigger the 429 Too Many Requests response:

require 'net/http'
require 'json'
require 'uri'

query = <<~GRAPHQL
  query {
    books {
      id
      title
      author
    }
  }
GRAPHQL

url = URI('http://localhost:4000/graphql')

success_count = 0
rate_limited_count = 0

105.times do |i|
  begin
    http = Net::HTTP.new(url.host, url.port)
    request = Net::HTTP::Post.new(url.path)
    request['Content-Type'] = 'application/json'
    request.body = { query: query }.to_json
    
    response = http.request(request)
    
    if response.code == '200'
      success_count += 1
      # Print full response only for the first successful request
      if success_count == 1
        data = JSON.parse(response.body)
        puts "Request ##{i + 1} (200 OK):"
        puts JSON.pretty_generate(data)
      end
    elsif response.code == '429'
      rate_limited_count += 1
      # Print a message only for the first rate-limited request
      if rate_limited_count == 1
        puts "\nRequest ##{i + 1}: Rate limited! (429 Too Many Requests)"
        puts "Response: #{response.body}"
      end
    end
    
  rescue StandardError => e
    puts "Error: #{e.message}"
  end
end

puts "\n--- Summary ---"
puts "Successful requests: #{success_count}"
puts "Rate-limited requests: #{rate_limited_count}"

The first 100 requests should succeed with a 200 status code, returning the list of books. After that, the remaining requests should be rejected with a 429 Too Many Requests response. Here's what the output looks like:

Request #1 (200 OK):
{
  "data": {
    "books": [
      { "id": "1", "title": "The Hobbit", "author": "J.R.R. Tolkien" },
      { "id": "2", "title": "Harry Potter", "author": "J.K. Rowling" }
    ]
  }
}

Request #101: Rate limited! (429 Too Many Requests)
Response: Too many requests

--- Summary ---
Successful requests: 100
Rate-limited requests: 5

The summary at the end confirms that exactly 100 requests were allowed through and the remaining 5 were blocked by our rate limiter.

Lesson Summary

In this lesson, we covered:

  • Defining a simple GraphQL schema using graphql-ruby's class-based approach.
  • Applying rate limiting middleware to a Sinatra app using Rack::Attack.
  • Integrating graphql-ruby with Sinatra to secure GraphQL APIs.
  • Testing the implementation to observe rate limiting in action.

By applying these techniques, you ensure your GraphQL API is more secure and can handle high loads in a stable manner. Well done on reaching the end of this lesson! Now it's time to put your knowledge into practice with the exercises that follow. Keep exploring and refining your skills in GraphQL and API security.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal