Rails 7.2 Adds Rate Limiting to Action Controller

Let’s understand what is Rate Limiting

Rate limiting is a technique used to control the rate of incoming requests or traffic to a server or a network. It helps in limiting the rate at which requests are processed which ensures system security and performance.

Restricting the rate of requests made by individual clients or IP addresses helps in preventing abuse, such as denial-of-service attacks or brute-force login attempts.

Now question is how to do this in Rails.

Before Rails 7.2

Before Rails 7.2, there were different ways to implement rate limiting that depended on specific requirements and constraints. One such way of it is by using rack-attack gem.

To use this gem, we need to create a new file config/initializers/rack_attack.rb and then we can write rate-limiting rules in this file.

For example, if we want to block access to all admins we can define rules in the following way:

# config/initializers/rack_attack.rb

Rack::Attack.blocklist("block all access to admin") do |request|
  # Requests are blocked if the return value is truthy
  request.path.start_with?("/admin")
end

In the above example request object is Rack::Request.

Another example is if we want to limit the rate of requests from certain IP addresses, we can do it like this:

# config/initializers/rack_attack.rb

Rack::Attack.throttle("requests by ip", limit: 3, period: 2) do |request|
  request.ip
end

Let’s see what is happening here, The Rack::Attack.throttle method is used to define rate-limiting rules.

  • requests by ip: This is the name of the rate-limiting rule. It serves as a unique identifier for the rule and can be used to reference it elsewhere in the configuration.
  • limit: This parameter specifies the maximum number of requests allowed within the defined period. In this case, the limit is set to 3 requests.
  • period: This parameter specifies the duration of the rate-limiting period in seconds. In this case, the period is set to 2 seconds.
  • request.ip: This line inside the block extracts the IP address of the incoming request. The rate limiting rule is applied per IP address, meaning each unique IP address is subject to its rate limit.

Now in Rails 7.2 we have an inbuilt API to handle Rate Limiting rules

Initially, DHH implemented this using the Kredis limiter type, which fully depends on the Redis server. So, as per this initial implementation, for the rate limit feature to work, the application should have a Redis server and Kredis 1.7.0+ available in the bundle.

This Kredis implementation is an incrementing counter value with a limit. Hence, Jean Boussier updated it with a more generic approach to use ActiveSupport::Cache.

Kredis (Keyed Redis)

Kredis provides abstractions or wrappers for organizing data in Redis using a single key. In Redis, keys are used to store and retrieve data, and Kredis enhances this by grouping related data structures and types under a single key.

It uses the Kredis limiter type underneath so in case Redis is inaccessible, the rate limit will not refuse action execution.

A limiter is a specialized version of a counter. While both counters and limiters can track the number of times an event or action occurs, a limiter adds additional functionality. Specifically, a limiter allows you to check whether a predefined limit has been exceeded, and it provides a fail-safe mechanism.

To know more about this implementation, please go through this pull request.

ActiveSupport::Cache

ActiveSupport::Cache module provides an interface for various caching mechanisms, which significantly improve the application’s performance.

The Cache Store is the storage mechanism where the cached data is kept. Rails support several built-in cache stores,including MemoryStore, FileStore, MemCacheStore, RedisCacheStore, NullStore.

To know more about this implementation, please go through this pull request.

About rate_limit method

Now, Rails 7.2 has the rate_limit method which can help us define rate-limiting rules directly in the Action Controller. It removes the dependency of using external gems.

The rate_limit method takes the following parameters:

  • to: The maximum number of allowed requests.
  • within: Time constraints within the maximum number of requests are allowed.
  • only: Controller Actions on which we want to apply rate limiting rules.
  • except: Controller Actions on which rate limiting rules will not apply.

Let’s take a look at how we can define it. In the below example, we are allowing 10 requests within 3 minutes to only create action.

class SessionsController < ApplicationController
  rate_limit to: 10, within: 3.minutes, only: :create
end

Here, Rate limits are by default unique to the IP address making the request, and requests that exceed the rate limit are refused with a 429 Too Many Requests response.

But what if we would like our custom logic instead of applying these rules just to IP address, and also if we want to modify our response?

For that, we get three additional parameters that we can use:

  • by: It takes a function that can be used to define our custom rate_limit logic.
  • with: It helps us modify our custom response when requests exceed the rate limit.
  • store: It defines which ActiveSupport::Cache mechanism should be used. By default, it uses config.cache_store. If you want to store rate limits in a different datastore than your general caches, you can pass a custom cache store in this parameter.

Let’s see the example below.

class LoginController < ApplicationController
  rate_limit to: 3, within: 1.minute, store: cache_store,
    by: -> { request.domain },
    with: -> { redirect_to home_path, alert: "Too many login attemps on domain!, Please try after some time" },
    only: :new
end

In the above example, we are defining rate limit rules with a given cache store to requests coming from specific domains and returning a modified 429 Too Many Requests response.

Need help on your Ruby on Rails or React project?

Join Our Newsletter