DEV Community

Yashika Vijayvargiya
Yashika Vijayvargiya

Posted on Originally published at Medium on

Rate Limiting in Rails 8.1: Protect Your APIs Without Rack::Attack

Have you ever wondered what happens when someone sends hundreds of requests to your login endpoint within a few seconds?

Without rate limiting, your application could face unnecessary database queries, excessive server load, brute-force login attempts, or API abuse.

Rate limiting helps control how frequently a client can access an endpoint within a specified time window.

The good news is that Rails provides a built-in rate_limit method, so you don't always need an additional gem to implement basic rate limiting.

Rails 8.1 also introduces a useful improvement: the scope: option, which allows multiple controllers to share the same rate-limit counter.

Let’s understand how it works with practical examples.

1. What Is Rate Limiting?

Rate limiting is a technique used to restrict the number of requests a client can make within a specific period.

For example:

  • Allow 5 login attempts within 3 minutes.
  • Allow 100 API requests per minute.
  • Allow 10 password reset requests within 15 minutes.

When a client exceeds the configured limit, the application rejects additional requests.

Usually, the server responds with:

HTTP 429 Too Many Requests
Enter fullscreen mode Exit fullscreen mode

This helps protect application resources and reduces abuse.

2. Rails Built-in Rate Limiting

Rails provides the rate_limit class method in Action Controller.

You can configure it directly inside a controller without installing an additional gem.

Let’s consider a login endpoint.

# app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  rate_limit to: 5, within: 3.minutes, only: :create

  def create
    # Login authentication logic
  end
end
Enter fullscreen mode Exit fullscreen mode

Let’s understand the configuration:

Option

Meaning

to: 5

Maximum 5 requests

within: 3.minutes

Within a 3-minute window

only: :create

Apply the limit only to the create action

By default, Rails identifies requests using the client’s IP address.

When the limit is exceeded, Rails raises ActionController::TooManyRequests, which Action Dispatch handles by returning HTTP 429.

3. Customize the Rate Limit Response

The default response might not be sufficient for an API application.

You can customize the response using the with: option.

class SessionsController < ApplicationController
  rate_limit to: 5,
             within: 3.minutes,
             only: :create,
             with: -> {
               render json: {
                 error: "Too many login attempts. Please try again later."
               }, status: :too_many_requests
             }

  def create
    # Login authentication logic
  end
end
Enter fullscreen mode Exit fullscreen mode

Now, when a client exceeds the configured limit, the API returns a JSON error response.

Example:

{
  "error": "Too many login attempts. Please try again later."
}
Enter fullscreen mode Exit fullscreen mode

With HTTP status:

429 Too Many Requests
Enter fullscreen mode Exit fullscreen mode

This is particularly useful when building APIs consumed by React, mobile applications, or other external clients.

4. Rate Limit Based on User Instead of IP

By default, Rails applies rate limiting based on the request IP address.

But what if you want to apply limits per authenticated user?

You can customize the identifier using the by: option.

class Api::BaseController < ApplicationController
  rate_limit to: 100,
             within: 1.minute,
             by: -> { current_user&.id || request.remote_ip }
end
Enter fullscreen mode Exit fullscreen mode

Here:

  • Authenticated users are identified by their user ID.
  • Unauthenticated requests are identified by their IP address.
  • Each identifier gets its own rate-limit counter.

This is useful when you want to provide individual API limits to authenticated users.

Important: If your application runs behind a proxy or load balancer, configure trusted proxy handling correctly. Otherwise, IP-based limits may not identify clients as expected.

5. What’s New in Rails 8.1? The scope: Option

This is one of the most useful improvements to the built-in rate-limiting functionality.

Before Rails 8.1, rate-limit counters were scoped to individual controller paths by default.

Consider an API application with two controllers:

Api::PostsController
Api::UsersController
Enter fullscreen mode Exit fullscreen mode

Suppose both controllers have the following configuration:

rate_limit to: 100, within: 1.minute
Enter fullscreen mode Exit fullscreen mode

By default, these controllers maintain separate counters.

This means a client could make:

  • 100 requests to the posts endpoint.
  • 100 requests to the users endpoint.

That’s 200 requests in total.

What if you want both controllers to share one common limit?

Rails 8.1 introduces the scope: option for this purpose.

Example: Shared rate limit across controllers

Create a common API controller:

# app/controllers/api_controller.rb

class ApiController < ApplicationController
  rate_limit to: 100,
             within: 1.minute,
             scope: "public_api"
end
Enter fullscreen mode Exit fullscreen mode

Now inherit from it:

class Api::PostsController < ApiController
  def index
    render json: Post.all
  end
end

class Api::UsersController < ApiController
  def index
    render json: User.all
  end
end
Enter fullscreen mode Exit fullscreen mode

Both controllers share the same rate-limit scope.

If a client makes 60 requests to the posts endpoint, only 40 requests remain for the users endpoint during the same rate-limit window.

The shared counter uses the same scope and client identifier.

Why is this useful?

Imagine an application exposing 20 public API endpoints. Instead of allowing 100 requests per endpoint, you can enforce a common limit across the entire API.

This gives you better control over overall API usage.

6. Multiple Rate Limits on the Same Controller

Sometimes one rate limit isn’t enough.

For example, you might want:

  • A short-term limit of 3 requests every 2 seconds.
  • A longer-term limit of 10 requests every 5 minutes.

Rails supports multiple rate limits on the same controller.

Use the name: option to distinguish them.

class SessionsController < ApplicationController
  rate_limit to: 3,
             within: 2.seconds,
             name: "short_term"
  rate_limit to: 10,
             within: 5.minutes,
             name: "long_term"
  def create
    # Login logic
  end
end
Enter fullscreen mode Exit fullscreen mode

Each limit maintains its own counter.

This is useful when you want to prevent both sudden bursts of traffic and repeated requests over a longer period.

7. Where Does Rails Store Rate-Limit Counters?

An important question is: How does Rails remember how many requests a client has already made?

Rails uses an ActiveSupport::Cache store to maintain rate-limit counters.

By default, it uses the controller cache store, which falls back to the application’s configured cache store.

For a production application running on multiple servers, using a shared Redis cache can be useful.

Example:

class ApiController < ApplicationController
  RATE_LIMIT_STORE = ActiveSupport::Cache::RedisCacheStore.new(
    url: ENV["REDIS_URL"]
  )
  rate_limit to: 100,
             within: 1.minute,
             store: RATE_LIMIT_STORE
end
Enter fullscreen mode Exit fullscreen mode

Now the rate-limit counters are stored in Redis.

Why does this matter?

Imagine your application runs on three application servers:

             Load Balancer
                   |
          -------------------
          | | |
        Server 1 Server 2 Server 3
Enter fullscreen mode Exit fullscreen mode

If each server maintains its own independent counters, a client might bypass the intended limit by sending requests to different servers.

Using a shared Redis cache allows all three servers to access the same counters.

Make sure Redis is configured with appropriate availability, memory, and eviction settings for your production workload.

8. Rails Rate Limiting vs Rack::Attack

Before Rails introduced built-in rate limiting, developers commonly used gems such as Rack::Attack.

Does Rails’ built-in functionality mean you no longer need Rack::Attack?

Not necessarily.

For straightforward controller-level rate limiting, Rails’ built-in functionality is often sufficient.

If you need advanced request filtering, blocklists, safelists, or more complex throttling rules, Rack::Attack is still worth considering.

9. When Should You Use Rails Built-in Rate Limiting?

Here are some practical use cases:

Login endpoints

Prevent repeated login attempts that could indicate brute-force activity.

Password reset endpoints

Avoid excessive password reset requests.

Public APIs

Control how frequently clients can access your API.

Registration endpoints

Reduce automated account creation attempts.

Expensive operations

Protect endpoints that perform costly database queries or external API calls.

Remember that rate limiting is only one layer of application protection. You should also implement authentication, authorization, input validation, monitoring, and appropriate infrastructure-level protections.

Conclusion

Rails makes basic rate limiting easier by providing the rate_limit method directly in Action Controller.

With Rails 8.1, the new scope: option makes it possible to share rate-limit counters across multiple controllers.

The key takeaways:

  • Use rate_limit to restrict requests within a time window.
  • Use by: to customize client identification.
  • Use with: to customize the response.
  • Use scope: to share limits across controllers.
  • Use store: to configure a shared cache such as Redis.
  • Use name: when defining multiple limits on one controller.

You don’t always need an additional gem to protect your Rails endpoints from excessive requests.

Sometimes, the framework already provides the functionality you need.

Have you used Rails’ built-in rate limiting, or do you prefer Rack::Attack? Share your experience in the comments!

References

Top comments (0)