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
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
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
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."
}
With HTTP status:
429 Too Many Requests
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
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
Suppose both controllers have the following configuration:
rate_limit to: 100, within: 1.minute
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
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
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
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
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
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!

Top comments (0)