In the dynamic world of web development, managing the flow of requests is crucial for maintaining a responsive and reliable application. Rate limiting is a powerful technique that acts as the traffic cop for your API, ensuring fair access to resources and preventing potential chaos. In a nutshell, rate limiting is the practice of controlling the rate of requests a user, device, or application can make within a set timeframe.
In this blog post, we'll delve into the concept of rate limiting, exploring its significance and implementation in Rails 8.0.
It's crucial in ensuring the following:
In the pre-Rails 8.0 era, the go-to solution for rate limiting was the
rack-attack
gem. This gem allowed
developers to set up rate-limiting rules by adding custom code in the
rack_attack.rb
file. While effective, this approach required manual
intervention for each endpoint and could become cumbersome in a growing
application. This external dependency brought its own set of challenges, such as
the need for custom code and constant vigilance over rate-limited endpoints.
Rails 8.0 brings a native rate-limiting feature to the Action Controller,
streamlining the process and eliminating the need for external gems. Developers
can now set rate limits directly within their controllers using the rate_limit
method.
Let's take a look at the usage:
Utilize the rate_limit
method within your controller actions, specifying the
maximum allowed requests and the corresponding timeframe. The rate_limit
method accepts the following parameters:
to
: The maximum number of requests allowed, beyond which the rate limiting
error will be raised, that is with a 429 Too Many Requests response.within
: The maximum number of requests allowed in a given time window.only
: Which all controller actions ought to be rate limited.except
: Which all controller actions to be omitted from rate limiting.An example:
class SignupController < ApplicationController
rate_limit to: 4, within: 1.minute, only: :create
def create
# ...
end
end
Target specific actions or employ custom logic for nuanced control. For instance, you can limit requests based on domain instead of IP address.
rate_limit to: 4, within: 1.minute, by: -> { request.domain }, only: :create
By default, rate-limited requests receive a 429 Too Many Requests
error.
However, you can personalize the response for a more informative user
experience.
rate_limit to: 4, within: 1.minute, with: -> { redirect_to(ip_restrictions_controller_url), alert: "Signup Attempts failed four times. Please try again later." }, only: :create
The Rails 8.0 rate-limiting implementation at the moment allows us to make use a wide range of backends as its store.
It includes:
The Rate Limiting API seamlessly integrates with Rails' caching mechanisms,
leveraging
ActiveSupport::Cache
stores. Developers can specify custom cache stores if they require separate
handling for rate limits compared to other cache data. This integration ensures
efficient storage and retrieval of rate limit data, optimizing performance. An
example:
class SignupController < ApplicationController
RATE_LIMIT_STORE = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])
rate_limit to: 8, within: 2.minutes, store: RATE_LIMIT_STORE
end
At the moment the built-in rate limiting feature is limited in its
extensibility. There are cases where this feature cannot be used as a direct
replacement for logic that's based on rack-attack
gem. A good example is a
scenario where we might want to use an exponential backoff based rate limiting
algorithm, which can be implemented using rack-attack
gem but not with the
built-in Rails feature at the moment.
Right now we are constrained to stick on to the default cache counter algorithm that Rails comes shipped with. In future we can expect a more generic limiter interface, allowing users to explore advanced rate-limiting algorithms such as exponential backoff or leaky bucket or token bucket etc. This change would open the door for developers to swap the implementation based on their specific requirements.
Please check out the following pull requests for more details:
If this blog was helpful, check out our full blog archive.