Upcoming changes in Rails rate limiter
A quick look at new rate-limiter features in the upcoming Rails version.
Historically we would use rack-attack for rate limiting, but Rails 7.2 introduced a built-in rate limiter:
class SignupController < ApplicationController
rate_limit to: 5, within: 1.minute
def create
render plain: "Signed up"
end
endUpcoming Rails 8.2 adds two useful improvements to the built-in rate limiter. I had mentioned them in my previous blog on rate limiting, but let us look at them in detail here.
Dynamic limit and window
Previously the limit to: and window within: parameters only accepted fixed values. Now with Rails 8.2, they can be a callable (method name, proc or lambda) allowing dynamic values:
class EmployeesController < ApplicationController
rate_limit to: :max_requests, within: :max_duration
def create
render plain: "Employee created"
end
private
def max_requests
current_user.admin? ? 1000 : 5
end
def max_duration
current_user.admin? ? 1.hour : 1.minute
end
endThis allows us to change the rate limit based on business logic.
Duck-typing cache-key object
By default, rate limits are keyed on remote_ip. The by: option lets you override this key.
For example, if we want to implement a per-user rate limit:
class ReportsController < ApplicationController
rate_limit to: 20, within: 1.minute, by: -> { "user/#{current_user.id}" }
endOr we could move the method to User model:
class User < ApplicationRecord
def cache_key
"user/#{id}"
end
endclass ReportsController < ApplicationController
rate_limit to: 20, within: 1.minute, by: -> { current_user.cache_key }
endNow with Rails 8.2 if by: is a callable and returns an object, cache_key method will be called on the returned object.
class User < ApplicationRecord
def cache_key
"rate-limit:user:#{id}"
end
endclass ReportsController < ApplicationController
# by: works because User responds to cache_key.
rate_limit to: 20, within: 1.minute, by: -> { current_user }
endIn this case, Rails will implicitly call current_user.cache_key and use it to group rate limits.
Now one might see a few problems with this cache_key approach:
Usermodel might have acache_keyfor another purpose (caching the user itself - not rate limit).- Rate-limit is not a model responsibility.
Since this is duck-typing, any object responding to cache_key works — it doesn't have to be an ActiveRecord model. A plain Ruby object works just as well:
UserRateLimit = Data.define(:user) do
def cache_key
"rate-limit:user:#{user.id}"
end
end
class ReportsController < ApplicationController
rate_limit to: 20, within: 1.minute, by: -> { UserRateLimit.new(current_user) }
endAll the code and tests can be found in this gist.
Available for hire
I am open to new roles. I bring 13 years of backend engineering experience, primarily in Ruby on Rails, with additional experience in TypeScript, Node.js, and Elixir.
Please have a look at my services.
Leads and referrals welcome — tejasbubane@gmail.com