Did you know that Rails now ships with native rate-limiting?
Previously, to protect your app from a barrage of requests, you'd use rack-attack, a popular rate-limiting gem. That functionality is now builtin to Rails, as of v7.2!
This native rate-limiting is what I currently use in production for AMZ Cart Share to protect our backend API endpoints.
Note:
Rack::Attackstill has its place, for advanced rules or Rack-layer blocking. This new, built-in rate-limiting is simpler. For most cases though, as you'll see, it's plenty.
For some quick context, this app has 2 systems which speak to each other: a cart-sharing Chrome extension + a backend API. The backend API is what I've protected with rate-limits.
It couldn't be simpler to add rate-limits to your controllers (below is copied almost verbatim from our codebase).
A single line will do it:
class Api::MyController < ApplicationController
rate_limit to: 10, within: 3.minutes, only: :create
def create; end
end
Breaking down the call to rate_limit:
to: The total number of requests that can be processed within the within time limit. Additional requests will return a 429 Too Many Requests HTTP response, and - in Rails 8+ - raise an ActionController::TooManyRequests error.within: The window of time within which we're constraining requests.only: You can optionally scope the rate limit to a specific controller endpoint using only and except. Without any scoping, the rate limit applies to every action in the controller.I'd highly recommend reading the full API reference for more info and some neat examples. For instance, you can define multiple rate-limits per controller over different time horizons:
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
rate_limit to: 3, within: 2.seconds, name: "short-term"
rate_limit to: 10, within: 5.minutes, name: "long-term"
def show; end
def create; end
end
There's more too: you can define custom uniqueness constraints with by: (default is per-ip), rate-limits spanning multiple controllers with scope:, custom error responses using with:... the list goes on!
Note: While controller-based rate limits were introduced in Rails 7.2,
name:andscope:were only introduced in Rails 8. Double-check your app version so you know which arguments you can pass.
Using rate_limit is essentially plug-and-play, apart from one bit of setup you need to do - ensuring you've configured a production cache store:
Rails needs somewhere to actually store all the info for tracking these rate limits.
From the API docs:
Rate limiting relies on a backing ActiveSupport::Cache store and defaults to config.actioncontroller.cachestore, which itself defaults to the global config.cache_store.
Essentially, you want to make sure you've set config.cache_store in your production config, config/environments/production.rb to something sensible.
If you leave this un-configured, Rails will default to file_store, which can work, but with some caveats:
If you want to use Redis instead, you can set it like this:
config.cache_store = :redis_cache_store, { url: ENV.fetch("REDIS_URL") }
Note: Rails 8 defaults to Solid Cache instead, which doesn't have the file_store drawbacks. If you're already using Solid Cache, you're already all set up!
For further reading, check out this short write-up, which digs a little bit deeper into this native rate limiting.
And don't forget to read the API docs! Seriously, they're great — short, sweet, and full of great examples.
You can also explore the implementation directly on GitHub.
First published on 28th April, 2026