Throttle api calls in Rails

Sometimes your application get overwhelmed with requests. These requests can block other users from accessing the application. To prevent this you can set a threshold on number of requests a user can make for any endpoint in given span of time.

I am using rack-attack gem to active this.

Add rack-attack gem. Add it in you Gemfile and run bundle install command.

Add rack_attack.rb file in initialisers with code snippet given below. Here we will be adding api rate limit for login api. I am allowing 10 requests per minute and 50 per day. You can add more api’s according to your requirement.

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 count of request made using given username and starts throwing error when specified request limit is crossed. User will be able to access the app again in next minute or day.

You can return custom response or error message when api request limit is reached. Add below code snippet 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 http response code from 500 series if you dont want to reveal user that you have a request limit in place.

You can put your domain logic in throttled_callback. Here I am notifying admin when 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 necessary have a user identifier in each request like contact_us, about_us. You can limit request for an IP address in such scenario.

Add below code snippet 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 help you to get started with.

Happy coding!!!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Kavita Jadhav

Kavita Jadhav

Application Developer @ ThoughtWorks Technologies