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.
The Need for Rate Limiting
It's crucial in ensuring the following:
- Security Shield: Guards against Denial-of-Service (DoS) attacks, thwarting malicious attempts to flood your app and crash it.
- Resource Balance: Prevents resource abuse by capping heavy users, ensuring fair resource distribution and maintaining optimal app performance.
- Brute-Force Defender: Stalls hackers attempting password guesses or exploiting vulnerabilities through relentless, repeated attempts.
How Rack::Attack Paved the Way
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: Native Rate Limiting
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:
Define Limits
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:
1class SignupController < ApplicationController 2 rate_limit to: 4, within: 1.minute, only: :create 3 4 def create 5 # ... 6 end 7end
Granular Control
Target specific actions or employ custom logic for nuanced control. For instance, you can limit requests based on domain instead of IP address.
1rate_limit to: 4, within: 1.minute, by: -> { request.domain }, only: :create
Tailored Responses
By default, rate-limited requests receive a 429 Too Many Requests error. However, you can personalize the response for a more informative user experience.
1rate_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
Custom Cache Stores
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:
- Memcached
- Redis (including alternative Redis clients like Dalli)
- Database-backed stores
- File-based stores
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:
1class SignupController < ApplicationController 2 RATE_LIMIT_STORE = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"]) 3 rate_limit to: 8, within: 2.minutes, store: RATE_LIMIT_STORE 4end
What to look forward to
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: