Throttle api calls in Rails
--
Sometimes your application gets overwhelmed with requests. These requests can block other users from accessing the application. To prevent this you can set a threshold on the number of requests a user can make for any endpoint in a given span of time.
I am using rack-attack gem to achieve it.
Add rack-attack gem. Add it to your Gemfile and run bundle install command.
Add rack_attack.rb file in initializers with code snippet given below. Here we will be adding an API rate limit for login API. I am allowing 10 requests per minute and 50 requests per day. You can add more API’s according to your requirements.
require "ipaddr"
Rack::Attack.cache.store = ActiveSupport::Cache::RedisStore.new(REDIS_URL)
class Rack::Attack
class Request < ::Rack::Request
def remote_ip
@remote_ip ||= (env["HTTP_REFERRER"] || env["action_dispatch.remote_ip"] || ip).to_s
end
def body_params
unless @body_params
@body_params = JSON.parse(body.read)
body.rewind
end
@body_params
end
def username
body_params["username"]
end
def login?
self.path == "/sign_in" && self.post?
end
end
throttle("username:limit-per-minute", limit: 10, period: 1.minute) do |req|
if req.login?
req.username
end
end
throttle("username:limit-daily", limit: 50, period: 1.day) do |req|
if req.login?
req.username
end
end
end
It will maintain a count of requests made using a given username and start throwing an error when the specified request limit is crossed. Users will be able to access the app again in the next minute or day.
You can return a custom response or error message when the API request limit is reached. Add code snippet below after Request class.
Rack::Attack.throttled_callback = lambda do |request|
[429, {}, [{error: 'Your request cannot be processed at this time. Please try later.'}]]
end
You can use the HTTP response code from the 500 series if you don't want to reveal user that you have a request limit in place.
Next, you can put your domain logic in the throttled_callback block. Here I am notifying the admin when the user reaches the limit.
Rack::Attack.throttled_callback = lambda do |request|
ApplicationMailer.notify_admin(to: SYSTEM_ADMIN_EMAIL, subject: 'Api call limit reached', body: "Username: #{request.body_params["username"]}").deliver
[429, {}, [{error: 'Your request cannot be processed at this time. Please try later.'}]]
end
You don’t necessarily have a user identifier in each request like contact_us, or about_us. You can limit requests for an IP address in such a scenario.
Add the code snippet below for that:
throttle("ip:login-limit-per-minute", limit: 10, period: 1.minute) do |req|
if req.contact_us?
req.remote_ip
end
end
Add contact_us? method in the Request class:
def contact_us?
self.path == "/contact_us" && self.get?
end
Hope this helps you to get started.
Happy coding!!!