DEV Community

Yaroslav Litvinov
Yaroslav Litvinov

Posted on

Built-in Rate Limiting in Rails 8

Rails 8 comes with simple built-in RateLimiting feature

Basic usage

You can apply a basic rate limit in your controller with the rate_limit directive:

class GreetingsController < ApplicationController
  rate_limit to: 5, within: 1.minute, by: -> { request.ip }

  def greet
    render json: {message: "Hello, #{name}!"}, status: :ok
  end

  def goodbye
    render json: {message: "Bye, #{name}!"}, status: :ok
  end

  private

  def name
    params.require(:name)
  end
end
Enter fullscreen mode Exit fullscreen mode

Multiple named rate limits

use :name argument when using multiple rate limits

class GreetingsController < ApplicationController
  rate_limit to: 2, within: 1.second, by: -> { request.ip }, name: "short-term"
  rate_limit to: 1000, within: 10.minutes, by: -> { request.ip }, name: "long-term"

  def greet
    render json: {message: "Hello, #{name}!"}, status: :ok
  end

  def goodbye
    render json: {message: "Bye, #{name}!"}, status: :ok
  end

  private

  def name
    params.require(:name)
  end
end
Enter fullscreen mode Exit fullscreen mode

Rate limits per action

assign limit to a specific action by using :only argument:

class GreetingsController < ApplicationController
  rate_limit to: 5, within: 1.minute, by: -> { request.ip }, name: "greet-limit", only: :greet
  rate_limit to: 6, within: 70.seconds, by: -> { request.ip }, name: "bye-limit", only: :goodbye

  def greet
    render json: {message: "Hello, #{name}!"}, status: :ok
  end

  def goodbye
    render json: {message: "Bye, #{name}!"}, status: :ok
  end

  private

  def name
    params.require(:name)
  end
end
Enter fullscreen mode Exit fullscreen mode

Adding Default Rate Limits

It's easy to extract the default rate limits into a module and include it in your controllers as default behavior.
We can use with option to define a custom error handler.

module DefaultRateLimits
  extend ActiveSupport::Concern

  included do
    rate_limit to: 45, within: 1.minute, by: -> { request.ip }, name: "long-term", with: -> { too_many_requests }
    rate_limit to: 3, within: 2.seconds, by: -> { request.ip }, name: "short-term", with: -> { too_many_requests }

    def too_many_requests(msg = nil)
      err = "Rate limit exceeded."
      if msg
        err += " #{msg}."
      end

      render json: {error: err}, status: :too_many_requests
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

include DefaultRateLimits in your controller:

class GreetingsController < ApplicationController
  include DefaultRateLimits

  rate_limit to: 3, within: 10.minutes, by: -> { request.params["name"] }, name: "greet-limit", only: :greet, with: -> { too_many_requests("Try again in 10 minutes") }
  rate_limit to: 5, within: 30.minutes, by: -> { request.params["name"] }, name: "goodbye-limit", only: :goodbye, with: -> { too_many_requests("Try again in 30 minutes") }

  def greet
    render json: {message: "Hello, #{name}!"}, status: :ok
  end

  def goodbye
    render json: {message: "Bye, #{name}!"}, status: :ok
  end

  private

  def name
    params.require(:name)
  end
end
Enter fullscreen mode Exit fullscreen mode

These defaults apply to all actions unless overridden by more specific rate limits.

In this example the 'greet' action will have 3 rate limits applied concurrently:

  • short-term: ip-based
  • long-term: ip-based
  • greet-limit: payload-based (params[:name])

The 429 response will look like:

{
  "error": "Rate limit exceeded. Try again in 30 minutes."
}
Enter fullscreen mode Exit fullscreen mode

Caching Strategies

Under the hood rate_limit uses Rails cache store (ActiveSupport::Cache)
We can override it by passing a custom store, e.g:

class APIController < ApplicationController
  RATE_LIMIT_STORE = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])
  rate_limit to: 10, within: 3.minutes, store: RATE_LIMIT_STORE
end
Enter fullscreen mode Exit fullscreen mode

In production environments, it's recommended to use distributed caching systems such as Redis or Memcached for better performance and scalability. For development purposes, you can use ActiveSupport::Cache::MemoryStore, which is simpler and runs in memory.

# config/application.rb or config/environments
config.cache_store = :memory_store, { size: 64.megabytes }
Enter fullscreen mode Exit fullscreen mode

Testing

Internally rate limit cache key implemented as:

def rate_limiting(to:, within:, by:, with:, store:, name:)
  cache_key = ["rate-limit", controller_path, name, instance_exec(&by)].join(":")
  count = store.increment(cache_key, 1, expires_in: within)
  if count && count > to
    ActiveSupport::Notifications.instrument("rate_limit.action_controller", request: request) do
      instance_exec(&with)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

see rate_limiting.rb

When testing your API you can set the cache counter manually. This allows you to simulate throttled requests without waiting for the limit to be naturally hit. In your request/controller spec:

before do
  cache_key = ["rate-limit", "greetings", "greet-limit", "Rachel"].join(":")
  Rails.cache.increment(cache_key, 10, expires_in: 1.minutes)
end

it "returns 429" do
  # Your test code here
end
Enter fullscreen mode Exit fullscreen mode

Don't forget to reset the cache after each test:

after do
  cache_key = ["rate-limit", "greetings", "greet-limit", "Rachel"].join(":")
  Rails.cache.delete(cache_key)
end
Enter fullscreen mode Exit fullscreen mode

or in rails_helper:

RSpec.configure do |config|
  config.after(:each) do |example|
    Rails.cache.clear
  end
end
Enter fullscreen mode Exit fullscreen mode

Summary

The RateLimiting module in Rails 8 offers a simple yet effective way to throttle requests, suitable for many applications from development through production.
It's easy to configure, helps prevent abuse, and integrates seamlessly with Rails controllers.
While ideal for most standard use cases, it may fall short in scenarios requiring dynamic rules, geo-based blocking, or distributed request coordination. For advanced rate limiting, consider libraries like rack-attack.

links:

Top comments (0)