ログインフォームに同じIPから何百回もパスワードを試されている。公開したAPIが一部の利用者から毎秒何十回も叩かれている。ログを見てヒヤッとしたこと、ありませんか?
こうした「連打」をはじくのがレート制限(流量制限)です。Railsでレート制限というと、これまでは Rack::Attack gem を入れるのが定番でした。ただ、ログインを少し守りたいだけなのに gem を1つ増やして設定ファイルを書くのは、少し重い。かといって放置するのも怖いですよね。
実は今のRailsには、コントローラに1行書くだけでレート制限がかけられる rate_limit が標準で入っています。gemは不要です。この記事では、ログインフォームとAPIを守る実例を軸に、基本からハマりどころまで通しで解説します。
まず結論:ログインを守る最小コード
細かい説明の前に、完成形を見てしまいましょう。Rails 8の認証ジェネレータが作る SessionsController を例にすると、ログインを守るのはこれだけです。
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
rate_limit to: 10, within: 3.minutes, only: :create
endこれで「同じIPからのログイン試行(create アクション)を3分間に10回まで」に制限できます。11回目のリクエストは処理されず、429 Too Many Requests が返ります。
やることは本当にこの1行だけです。カウントの保存先やキーの持ち方は、あとからオプションで細かく調整できます。まずは全体像を図で押さえておきましょう。

rate_limitはどのバージョンから使える?
先にバージョンの前提を整理しておきます。ここを勘違いすると、公式ドキュメントのコードを写しても動かない、ということが起きます。
| バージョン | できること |
|---|---|
| Rails 7.2 | rate_limit が ActionController に追加。1コントローラにつき制限は1つまで |
| Rails 8.0 | name: オプションが追加。1コントローラに複数の制限を併用できるようになった |
| Rails 8.1 | 超過時の既定の挙動が変更(後述。例外を投げる方式に) |
つまり rate_limit 自体はRails 7.2から使えます。ただし「短期と長期の制限を同時にかける」といった name: を使う書き方はRails 8.0以降です。この記事はRails 8.1を前提に進めます。
基本の3つ:to / within / only
まずは最小構成で使った3つの指定を分解します。レート制限の挙動は、ほぼこの3つで決まります。
to:… 許可する回数。「何回まで」の部分within:… 時間窓。「どのくらいの期間で」の部分(3.minutesのようにActiveSupportの期間で書ける)only:… 対象アクション。before_actionと同じでexcept:も使える。省略するとコントローラの全アクションが対象
「to: 10, within: 3.minutes」なら「3分間に10回まで」。回数と期間はセットで考えます。ログインなら厳しめ(3分で10回)、一般的なフォーム送信ならゆるめ(1分で30回)など、守りたいものに合わせて調整してください。
超過したら何が起きる?(429とTooManyRequests)
上限を超えたリクエストがどう扱われるかは、地味ですがRailsのバージョンでAPIの挙動が違う部分です。
Rails 8.1では、超過すると ActionController::TooManyRequests という例外が投げられます。この例外はAction Dispatchが受け止めて、最終的に 429 Too Many Requests のレスポンスに変換してくれます。rescue_from ActionController::TooManyRequests で自分で捕まえることもできます。
Rails 7.2〜8.0では例外ではなく、既定で head :too_many_requests(中身のない429レスポンス)を直接返す挙動でした。どちらのバージョンでも「超過すると429が返る」という結果は同じなので、まずはそこを押さえておけば十分です。
オプション総まとめ:by / with / store / name
基本の3つに加えて、実務で効いてくるのが次の4つのオプションです。まず一覧で俯瞰してから、1つずつ実例を見ていきます。
| オプション | 役割 | 既定値 |
|---|---|---|
by: | カウントのキー(誰ごとに数えるか) | リクエスト元のIP(request.remote_ip) |
with: | 超過したときの挙動を差し替える | 429を返す(8.1は例外経由) |
store: | カウントの保存先(キャッシュストア) | config.action_controller.cache_store |
name: | 制限に名前を付けて複数併用する(Rails 8.0〜) | なし(単一の制限) |
by:IP以外のキーで数える
by: は「誰ごとにカウントするか」を決めます。既定はIPアドレスです。callable(ラムダ)を渡せば、好きなキーで数えられます。
たとえばAPIなら、IPではなくAPIキー単位で数えたいことが多いはずです。
rate_limit to: 100, within: 1.minute,
by: -> { request.headers["X-Api-Key"] || request.remote_ip }ここで1つ注意があります。ログイン保護で「メールアドレス単位」で数えたくなりますが、攻撃者が他人のメールアドレスを連打すると、その人をわざとロックアウトできてしまうという弱点があります。メールアドレスで数える制限を入れるなら、IP単位の制限も別に併用して守るのが安全です(この併用は後述の name: でやります)。
with:超過時のレスポンスを好きに作る
既定の429は中身のないレスポンスです。ログイン画面ならエラーメッセージ付きでリダイレクト、APIならJSONで理由を返す、といった出し分けを with: で指定できます。
# 画面向け:メッセージを出してログイン画面に戻す
rate_limit to: 10, within: 3.minutes, only: :create,
with: -> { redirect_to new_session_path, alert: "試行回数が多すぎます。少し待ってから試してください。" }
# API向け:JSONで429を返す
rate_limit to: 100, within: 1.minute,
with: -> { render json: { error: "rate limit exceeded" }, status: :too_many_requests }store:カウントをどこに保存するか
rate_limit は「このキーが時間窓の中で何回来たか」をキャッシュに記録して数えています。既定の保存先は config.action_controller.cache_store(最終的には config.cache_store)です。
ここが最大のハマりどころなので、独立した章で後述します。まずは「別のストアを明示的に指定できる」とだけ覚えてください。Redisを使う例はこうなります。
class Api::MessagesController < ApplicationController
RATE_LIMIT_STORE = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])
rate_limit to: 100, within: 1.minute, store: RATE_LIMIT_STORE
endname:短期と長期、2段構えで守る(Rails 8の目玉)
Rails 8.0で入った name: が、個人的にいちばん実用的だと思うオプションです。名前を付けることで、同じコントローラに複数の制限を重ねられます。
レート制限は「短期の連打」と「長期の総当たり」で、ちょうどいいしきい値が違います。両方を1つの制限で表そうとすると、どうしても緩すぎたり厳しすぎたりします。name: なら、それぞれ別に設定して重ねられます。
class SessionsController < ApplicationController
# 短期バースト対策:20秒に5回まで(連打を止める)
rate_limit to: 5, within: 20.seconds, name: "burst", only: :create
# 長期の総当たり対策:1時間に20回まで(じわじわ試す攻撃を止める)
rate_limit to: 20, within: 1.hour, name: "sustained", only: :create
end
Rails 7.2には name: がなく、制限は1コントローラに1つだけでした。2つ書いても片方しか効かない(内部のキーが衝突する)ので、この2段構えはRails 8.0以降の書き方だと覚えておいてください。
ハマりどころ:キャッシュストアで消える落とし穴
ここが実務でいちばん事故る部分です。「コードは書いたのに、まったく制限がかからない」という時、だいたい原因はキャッシュストアです。
開発環境で効かない:null_store問題
Rails の開発環境では、キャッシュが既定でオフになっています。このとき cache_store は :null_store(何も保存しないストア)なので、rate_limit がいくら数えようとしてもカウントが毎回ゼロに戻り、制限が一切かかりません。
開発環境で動作確認したいときは、キャッシュを有効にします。
# 開発環境のキャッシュをON(tmp/caching-dev.txt が作られる)
bin/rails dev:cache「本番では効くのに手元で再現しない」の正体はたいていこれです。テストで挙動を確認したいときも、一時的に ActiveSupport::Cache::MemoryStore に差し替えると安定します。
本番で効かない:プロセス・サーバーをまたげない
本番側の落とし穴はこちらです。ストアがメモリ上のキャッシュ(MemoryStore)だと、カウントがプロセスごとにバラバラになります。
PumaのワーカーやサーバーのインスタンスをN個に増やすと、同じIPのリクエストが別々のプロセスに振り分けられ、それぞれが別々にカウントします。結果、実質的に制限がN倍ゆるくなるのです。
これを防ぐには、全プロセスで共有できるストアが必要です。選択肢は主に2つです。
| ストア | 向いているケース |
|---|---|
| Solid Cache(Rails 8標準・DBが保存先) | Redisを増やしたくない小〜中規模。cache_store をSolid Cacheにしていれば既定のままで共有が効く |
Redis(RedisCacheStore) | すでにRedisがある/高頻度アクセスで速度を優先したい場合 |
Rails 8は標準のキャッシュがSolid Cache(DBベース)なので、特別な設定をしなくても共有ストアになっているケースが多いです。Solid系のストアとデータベース設定についてはRails8に入るsolid_queueなどのgemを使った時のdatabase.ymlの書き方にまとめています。
プロキシの後ろでIPが同じになる
もう1つ、by: の既定であるIP(request.remote_ip)がらみの注意点です。Nginxやロードバランサーの後ろにRailsを置くと、適切に設定しないと全リクエストのIPがプロキシのIPに見えてしまい、利用者全員が同じキーでまとめて数えられてしまいます。
X-Forwarded-For を正しく扱う設定(config.action_dispatch.trusted_proxies など)が要ります。そもそも前段にNginxを置くべきかという話はRailsの前段にNginxは必要なのかで整理しているので、構成を考えるときに合わせてどうぞ。
Rack::Attackとの使い分け
「じゃあ Rack::Attack はもう要らないの?」という疑問が出ると思います。結論は「守りたいものによる」です。両者は守る場所が違います。
| rate_limit(標準) | Rack::Attack(gem) | |
|---|---|---|
| 動く場所 | コントローラ(Railsの中) | Rackミドルウェア(Railsの手前) |
| 設定の単位 | アクション単位で直感的に書ける | アプリ全体のルールを一元管理 |
| 得意なこと | 特定フォーム・APIのピンポイント制限 | IP遮断(ban)・全体の防御・許可リスト |
| 依存 | なし(標準装備) | gem追加が必要 |
ログインやサインアップなど「このアクションを守りたい」がはっきりしているなら rate_limit で十分です。逆に、悪質なIPをアプリに入れる前に丸ごと遮断したい、全エンドポイントに横断的なルールを敷きたい、という段階になったら Rack::Attack の出番です。小さく始めて、必要になったら足す、で問題ありません。
導入チェックリスト
最後に、入れるときに確認したいポイントをまとめます。
- □ 守りたいアクションを
only:で絞ったか(全アクションにかけて正常利用を妨げていないか) - □
to:/within:は正常な利用を邪魔しない値か(ログインは厳しめ、閲覧系はゆるめ) - □ 本番のストアはプロセス・サーバー間で共有できるか(Solid Cache / Redis)
- □ 開発で確認するなら
bin/rails dev:cacheでキャッシュをONにしたか - □ プロキシの後ろなら、IPが正しく取れる設定になっているか
- □ 超過時のメッセージは利用者に不親切すぎないか(
with:で調整)
まとめ
Railsのレート制限は、rate_limit to: 10, within: 3.minutes の1行から始められます。gemを足さずに、ログインの連打もAPIの乱打も止められるのは大きな利点です。
実務で効くポイントは、①本番は共有ストア(Solid Cache / Redis)を使う、②name: で短期と長期を2段構えにする、③超過時の見せ方を with: で整える、の3つです。まずはログインフォームに1行入れてみて、挙動を確かめるところから始めてみてください。
Rails 8の認証まわりをこれから作る方は、Rails 8の認証ジェネレータとDeviseはどっちを使う?やマジックリンク(パスワードレス)ログインの実装方法も合わせて読むと、ログイン周辺の守りを一通り固められます。
スポンサーリンク
