Redis を用いた素朴なIPベースのRate Limit を実装したRack Middleware。

https://github.com/toshimaru/Study/tree/main/ruby/rack/ratelimit

# app.rb
require_relative 'redis_rate_limitter'

class App
  def call(env)
    [ 200, {}, ["hello"] ]
  end
end

use RedisRateLimiter, limit: 10, window: 60
run App.new
require 'redis'

class RedisRateLimiter
  def initialize(app, limit: 60, window: 60)
    @app = app
    @limit = limit
    @window = window
    @redis = Redis.new
  end

  def call(env)
    request = Rack::Request.new(env)
    client_ip = request.ip

    key = "rate_limit:#{client_ip}"
    count = @redis.get(key).to_i

    if count >= @limit
      [429, { 'Content-Type' => 'text/plain' }, ["Rate limit exceeded. Try again later."]]
    else
      @redis.multi do |transaction|
        transaction.incr(key)
        transaction.expire(key, @window) if count == 0
      end
      @app.call(env)
    end
  end
end

Redis公式のベストプラクティス

その他のアルゴリズムの検討

今回実装したのは固定ウィンドウカウンタの制限アルゴリズム。

トークンバケットやリーキーバケットは LUA スクリプトを使う実装が必要っぽくて、手間がかかるっぽい。

Rails 7.2 から標準でレートリミット機能が搭載される

詳しい解説はこちら: Rails 7.2 Adds Rate Limiting to Action Controller | Saeloun Blog

class SessionsController < ApplicationController
  rate_limit to: 10, within: 3.minutes, only: :create
end

ref. ActionController::RateLimiting::ClassMethods | RailsDoc(β)

導入経緯も面白くて、DHHがKredisに依存したかたちで初期実装を行い、その後キャッシュレイヤーを抽象化したActiveSupport::Cacheで書き換えられているのが面白い。

これによりCacheバックエンドがRedis以外の様々なものに対応できるようになった。素晴らしい抽象化が働いた例だと思う。

関連記事