Rails 8.0 adds rate limiting to ActionController

railsJanuary 16, 2024Dotby Alkesh Ghorpade

Rate limiting is a technique used to control the rate of requests or actions a user, device, or application can make within a specific timeframe. It's like having a traffic light for your online resources, ensuring everyone gets a fair chance and preventing congestion.

It's the practice of putting a cap on the number of requests a user or device can make to your app within a specific timeframe. This helps prevent:

  • Denial-of-service attacks (DoS): Malicious actors flooding your app with requests, overwhelming its resources and crashing it.

  • Resource abuse: Heavy users consuming disproportionate resources, impacting legitimate users and app performance.

  • Brute-force attacks: Hackers attempting to guess passwords or exploit vulnerabilities through repeated attempts.

Before Rails 8.0

No built-in method was available to implement rate limiting on the number of requests.

One approach to implement rate limiting was to use the rack-attack gem.

How to use Rack::Attack in Rails to implement rate-limiting

Add rack-attack to Gemfile

The first step is to add the rack-attack to Gemfile and execute the bundle install command.

gem "rack-attack"

Add it to your Rails middleware stack

In your config/application.rb, add the below line inside the Application class.

config.middleware.use Rack::Attack

Configure rate-limiting rules

To add the rate-limiting rules, create a file named rack_attack.rb inside the config/initializers directory and add your rules.

A small example of adding a rate-limiting of 10 requests per minute from a particular ID address is as follows:

# config/initializers/rack_attack.rb

Rack::Attack.throttle('requests by ip', limit: 10, period: 1.minute) do |req|
  req.ip
end

If you want to set a rate-limiting for the number of login attempts, you can add a custom code as below:

Rack::Attack.throttle('login attempt', limit: 3, period: 1.minute) do |req|
  req.params['email'].presence if req.path == '/login' && req.post?
end

The rack-attack gem is helpful for rate limiting, but for a growing application, this approach is ideal. You might have to add a lot of custom code for the endpoints you need to add rate limiting. You would need to navigate to rack_attack.rb file every time to check which actions in your controller are rate limited.

In Rails 8.0

Rails 8.0 adds rate limiting to the Action Controller.

You need to set the rate_limit function in your controller. The rate_limit function accepts the following parameters:

  • to: The maximum number of requests allowed, beyond which the rate limiting error will be raised.

  • within: The maximum number of requests allowed in a given time window.

For example, to set a rate limit of 3 sign-up attempts within 1 minute, you need to add the below code. Let's say your Rails application has a LoginController, you can explicitly set the rate limit for the new action.

class LoginController < ApplicationController
  rate_limit to: 3, within: 1.minute, only: :new

  def new
    ...
  end
end

You can set the rate limit to specific actions in the controller by passing the only or except option.

by: and with: optional parameters

By default, rate limits are based on IP addresses, but you can pass your custom function using the by: option. If you want to rate limit the action by request domain instead of IP, you can pass the by: option.

class LoginController < ApplicationController
  rate_limit to: 3, within: 1.minute, by: -> { request.domain }, 
    only: :new

  def new
    ...
  end
end

To prevent overload, requests that surpass the rate limit are turned away with a 429 Too Many Requests error. You have the flexibility to create unique responses for these situations by supplying a callable object using the with: parameter.

class LoginController < ApplicationController
  rate_limit to: 3, within: 1.minute, 
    with: -> { redirect_to home_path, alert: "3 Login Attempts failed. Please try after 12 hours" }, 
    only: :new

  def new
    ...
  end
end

Note:

The rate limiting implementation leverages a Redis server with Kredis 1.7.0+ for storage. The Kredis limiter type, incorporating a failsafe mechanism, guarantees action execution proceeds even in Redis inaccessibility scenarios.

If your application has Kredis below 1.7.0 version, it will raise the below error:

Rate limiting requires Kredis 1.7.0+. Please update by calling `bundle update kredis`.

If the application has no Kredis the error below gets raised.

Rate limiting requires Redis and Kredis. Please ensure you have Redis installed on your system and the Kredis gem in your Gemfile.

To know more about this feature, please refer to this PR.

You can refer the below video for more details.

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at justin@shakacode.com or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right