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:
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:
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.
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 usesconfig.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.
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.