We will use Rails to initialize the project demo-rack-throttle-redis-simple:
# Create project
$ rails new demo-rack-throttle-redis-simple
$ cd demo-rack-throttle-redis-simple
# Install the required gem
$ bundler add rack-throttle
# Scaffold a route to test against
$ bin/rails g controller hello index
At this stage, our project is ready to update configuration.
Setting up our configuration
We will add some basic rules and remove the default forgery protection in the config/application.rb file:
require_relative 'boot'
require 'rails/all'
require 'rack/throttle'
require 'redis'
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module DemoRackThrottleRedisSimple
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
# Set this off so we can ping the endpoint
config.action_controller.default_protect_from_forgery = false if ENV['RAILS_ENV'] == 'development'
# Setting rules and configuration for our `rack-throttle` middleware.
rules = [
{ method: 'POST', limit: 5 },
{ method: 'GET', limit: 10 },
{ method: 'GET', path: '/hello', limit: 1 }
]
default = 10
config.middleware.use Rack::Throttle::Rules, cache: Redis.new, rules: rules, default: default
end
end
Here we are setting baseline rules for our application. We are setting the rules to be an array of hashes. Each hash will have a method and a limit value.
In particular, the third rule for path /hello will override the GET limit to 1 which we will see in the demonstration later.
Setting up our routes
Update config/routes.rb:
Rails.application.routes.draw do
resources :hello, only: [:index]
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
# root "articles#index"
end
This will enable a GET request at /hello.
Setting up our controller
Update app/controllers/hello_controller.rb:
class HelloController < ApplicationController
def index
render json: { message: 'Hello, World!' }
end
end
We will simply send a Hello, World! message back to the client if we are not rate-limited.
Testing our rate limiting
Start a rails server with rails s.
At first, I will be using ab (ApacheBench) to test our rate-limiting, but any tool will work.
Running 6 requests at the same time will result in a 429 response for 5 of the request. See the following:
$ ab -n 6 http://localhost:3000/hello
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient).....done
Server Software:
Server Hostname: localhost
Server Port: 3000
Document Path: /hello
Document Length: 27 bytes
Concurrency Level: 1
Time taken for tests: 0.080 seconds
Complete requests: 6
Failed requests: 5
(Connect: 0, Receive: 0, Length: 5, Exceptions: 0)
Non-2xx responses: 5
Total transferred: 1698 bytes
HTML transferred: 122 bytes
Requests per second: 74.61 [#/sec] (mean)
Time per request: 13.403 [ms] (mean)
Time per request: 13.403 [ms] (mean, across all concurrent requests)
Transfer rate: 20.62 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.2 0 1
Processing: 10 13 4.3 11 21
Waiting: 10 13 4.3 11 21
Total: 10 13 4.4 11 22
Percentage of the requests served within a certain time (ms)
50% 11
66% 11
75% 15
80% 15
90% 22
95% 22
98% 22
99% 22
100% 22 (longest request)
The above tells us that we had 5 failed requests.
If you check the Rails server logs, you will see something similar to the following:
Started GET "/hello" for ::1 at 2022-03-02 16:56:06 +1000
Processing by HelloController#index as */*
Completed 200 OK in 1ms (Views: 0.2ms | ActiveRecord: 0.0ms | Allocations: 114)
Started GET "/hello" for ::1 at 2022-03-02 16:56:06 +1000
Started GET "/hello" for ::1 at 2022-03-02 16:56:06 +1000
Started GET "/hello" for ::1 at 2022-03-02 16:56:06 +1000
Started GET "/hello" for ::1 at 2022-03-02 16:56:06 +1000
Started GET "/hello" for ::1 at 2022-03-02 16:56:06 +1000
The last 5 requests did not complete.
Seeing the Redis cache in action
To see our throttling as it happens, we can open another terminal and run redis-cli monitor.
Do so and re-run the ab command above. You will see logs similar to the following for the monitor: