The second part of this series can be found here.
Rate limiting or throttling is the practice of limiting how frequently legitimate users of a web service can access the service. Rate limiting is often put in place to prevent the hogging of resources by a sub set of the users of the system. Rate limiting works by responding with error messages when a client exceeds their allocated share of requests within a predefined time window. In addition to the error message, the response should also include information as to when rate limit will be reset so that the client can continue accessing the system after the reset.
What we need is a way to record the number of requests each client is making and reset this number to zero after a predefined time period and decide for each request whether the client making the request has exceeded the limits.
The test application
We will create a simple Rails application with a single API endpoint at foo.json
. The code needed for this is as follows:
Routes:
# config/routes.rb
RailsThrottle::Application.routes.draw do
get 'foo.json' => 'foo#index'
end
The controller:
# app/controllers/foo_controller.rb
class FooController < ApplicationController
def index
render json: {foo: :bar}
end
end
Storing the rate data
We need a place to store each client’s IP address and the number of requests it made. We need to increment this count for each request and reset the count to zero after a time period. Considering these needs, Redis is a great fit for this data store. Redis stores key value pairs and allows expiry time to be specified for each entry. Redis also comes with an INCR
1 command that ensures that increment operations are atomic. This will be useful to us if we were to run multiple instances of our app behind a load balancer.
To setup the application to use Redis, we will need to install the redis
2 gem. Once we have the gem, we will add a new initializer
named throttle.rb
which configures our Redis client.
# config/initializers/throttle.rb
require "redis"
redis_conf = YAML.load(File.join(Rails.root, "config", "redis.yml"))
REDIS = Redis.new(:host => redis_conf["host"], :port => redis_conf["port"])
This will load the Redis server’s host and port from the configuration file located at config/redis.yml
. This file will look like this:
# config/redis.yml
host: localhost
port: 6379
Using a before_filter
for rate limiting.
The first step is to log the number of requests each client is making. This can easily be achieved with a before_filter
3. Let’s add the filter into the ApplicationController
.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
...
before_filter :throttle
def throttle
client_ip = request.env["REMOTE_ADDR"]
key = "count:#{client_ip}"
count = REDIS.get(key)
unless count
REDIS.set(key, 0)
end
REDIS.incr(key)
true
end
...
end
Since this before_filter
belongs to the ApplicationController
, it will be applied to all requests, unless a specific controller chooses to skip it. So before every request is processed, the filter grabs the client’s IP and checks whether there is a count in Redis for this IP. If there is no count key, it creates one. Finally it increments the count.
At this point, the filter just records the requests made, but does not limit requests. Let’s go ahead and implement limiting. We need to specify the time window for rate limiting and how many requests should be allowed in that time window. We will allow a client a maximum of 60
requests in 15
minutes. The following constants need to be defined in throttle.rb
.
THROTTLE_TIME_WINDOW = 15 * 60
THROTTLE_MAX_REQUESTS = 60
The filter needs to be changed to respond with error messages when the rate limit is exceeded.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
...
before_filter :throttle
def throttle
client_ip = request.env["REMOTE_ADDR"]
key = "count:#{client_ip}"
count = REDIS.get(key)
unless count
REDIS.set(key, 0)
REDIS.expire(key, THROTTLE_TIME_WINDOW)
return true
end
if count.to_i >= THROTTLE_MAX_REQUESTS
render :status => 429, :json => {:message => "You have fired too many requests. Please wait for some time."}
return
end
REDIS.incr(key)
true
end
...
end
When the limit is reached, subsequent requests will be responded with an error message and the HTTP status code 429
. The 429 4 status code indicates that the user has sent too many requests in a given amount of time.
Let’s go ahead and test this.
bash$ for i in {1..100}
do
curl -i http://localhost:3000/foo.json >> /dev/null
done
bash$ less log/development.log | grep "200 OK" | wc -l
60
bash$ less log/development.log | grep "429 Too Many Requests" | wc -l
40
As you can see after 60
requests, all requests get the 429
response.
Improvements
While what we have implemented limits the requests, it does not give the client enough information as to how long it has to wait before making requests again. It would also be helpful if the server tells the client on each request how many total requests it is allowed to make in a window and how many more requests it can perform before limiting kicks in. We will look at this in the next blog post.
A sample application with this rate limiting in place is on GitHub.
- Redis documentation for INCR command. [return]
- redis - A Ruby client that tries to match Redis’ API one-to-one, while still providing an idiomatic interface. It features thread-safety, client-side sharding, pipelining, and an obsession for performance. [return]
- Rails’ before filter. [return]
- IETF: Additional HTTP Status Codes - 429 Too Many Requests. [return]