Action Cableは、Ruby on RailsにWebSocketを組み込むためのフレームワークです。チャット・通知・共同編集のように、サーバー側の変化をリロードなしでブラウザへ届けたいときに使います。本記事は2026年9月24日公開のRails 8.1.4を前提に、実装・認証・アダプタの選び方・再接続・テストをRailsのソースで確かめて解説します。
まとめ:Action Cableの要点
- Action CableはRails標準のWebSocket機能で、gem名は
actioncable、JavaScript側は@rails/actioncable。現行はRails 8.1.4(2026年9月24日) - 構成要素はコネクション(1つのWebSocket接続)・チャネル(購読の単位)・ブロードキャスト(配信先の名前)の3つ。サーバーは
stream_forで購読させ、broadcast_toで送る - 本番のアダプタは、Rails 8で新規作成したアプリならSolid Cable(DBをポーリング)が既定。Redis・PostgreSQLも選べる。開発用の
asyncは本番で使わない - PostgreSQLアダプタのペイロードは、既定設定では1通8000バイト未満。日本語の配信名を使うなら、長さ判定の不具合を直した8.1.4へ上げる
- サーバーは3秒ごとにpingを送る。クライアントは定期的な監視のたびに、最後の受信から6秒を超えていれば接続を張り直す
- サーバーからの一方向配信だけならSSE、Hotwire構成ならTurbo Streamsの方が手数が少ない
Action Cableの仕組み:コネクション・チャネル・ブロードキャスト
HTTPでは、ブラウザが要求して初めてサーバーが応答します。Action Cableはブラウザと /cable の間でWebSocket接続を張りっぱなしにし、サーバー側の任意のタイミングでデータを押し出します。WebSocketそのものの仕組みはWebSocketとは?仕組み・ハンドシェイクからNode.js実装・運用の判断まで実装者向けに解説で扱っています。
| 用語 | 実体 | 役割 |
|---|---|---|
| コネクション | ApplicationCable::Connection |
接続1本ごとの認証とユーザー特定 |
| コンシューマ | createConsumer() |
ブラウザ側の接続。通常はタブ内で共有 |
| チャネル | ChatChannel など |
購読の単位。MVCのコントローラーに相当 |
| サブスクリプション | consumer.subscriptions.create |
コンシューマがチャネルを購読した状態 |
| ブロードキャスト | "chat:Z2lk..." などの名前 |
配信先。購読者全員に届く |
| サブスクリプションアダプタ | Solid Cable/Redis など | サーバープロセス間でメッセージを中継 |
1本のコネクションの上に複数のチャネル購読が乗るので、チャット画面と通知バッジを同時に使ってもWebSocket接続は1本で足ります。ブロードキャストは「どの接続に」ではなく「どの名前を購読している全員に」送る指定です。ユーザー単位で送りたいときは、ユーザーごとの名前を購読させます。
Rails 8.1でのチャット実装手順
ルーム単位のチャットを例に、チャネル・クライアント・配信の3か所を順に書きます。ユーザー認証はRails 8の認証ジェネレーター(bin/rails generate authentication)で作ってある前提です。ジェネレーターが作るのはUserとSessionだけなので、Room・Message・メンバーシップのモデル(current_user.rooms・room.messagesが引ける関連)と、Userの表示名 name は別途用意しておきます。
チャネルの生成と購読時の権限確認
bin/rails generate channel Chat speak を実行すると、app/channels/chat_channel.rb と app/javascript/channels/chat_channel.js ができます。importmap構成なら config/importmap.rb に @rails/actioncable のpinも追加されます。サーバー側は次のように書き換えます。
class ChatChannel < ApplicationCable::Channel
def subscribed
@room = current_user.rooms.find_by(id: params[:room_id])
return reject unless @room
stream_for @room
end
def speak(data)
@room.messages.create!(user: current_user, body: data["body"])
end
end
subscribed は購読のたびに呼ばれます。ここで所属していないルームを reject しないと、ルームIDを書き換えるだけで他人の会話を受信できてしまいます。stream_for @room はモデルのGlobalIDから chat:Z2lkOi8v... 形式の配信名を作ります。Rails 8.1.0からは stream_for [group, group.owner] のように配列で複合キーも渡せます。
クライアントの購読とperform
ジェネレーターが作る chat_channel.js は、読み込まれた全ページで購読を始めます。チャット画面だけで購読し、画面を離れたら解除するには、Rails 8の既定に含まれるStimulusのコントローラーに移すのが扱いやすい方法です。この場合、chat_channel.js と channels/index.js 内のそのimport行は消しておきます。
// app/javascript/controllers/chat_controller.js
import { Controller } from "@hotwired/stimulus"
import consumer from "channels/consumer"
export default class extends Controller {
static targets = ["messages", "input"]
static values = { roomId: Number }
connect() {
this.subscription = consumer.subscriptions.create(
{ channel: "ChatChannel", room_id: this.roomIdValue },
{ received: (data) => this.append(data) }
)
}
disconnect() {
this.subscription?.unsubscribe()
}
speak(event) {
event.preventDefault()
if (this.subscription.perform("speak", { body: this.inputTarget.value })) {
this.inputTarget.value = ""
}
}
append(data) {
const li = document.createElement("li")
li.textContent = `${data.user}: ${data.body}`
this.messagesTarget.append(li)
}
}
<%# app/views/rooms/show.html.erb %>
<div data-controller="chat" data-chat-room-id-value="<%= @room.id %>">
<ul data-chat-target="messages"></ul>
<form data-action="submit->chat#speak">
<input name="body" data-chat-target="input">
</form>
</div>
perform("speak", {...}) は、サーバー側の同名メソッド speak(data) を呼びます。data には送った値に加えて "action" => "speak" が入ります。perform の戻り値は「WebSocketが開いていて送信できたか」で、未接続なら false が返ります。上の例で入力欄を消す条件にしているのはこのためです。ただし true はサーバーで保存できたことまでは意味しません。受信した文字列は textContent で入れ、innerHTML に直接渡さないでください。他のユーザーの入力がそのままHTMLとして実行されます。
モデル保存後のブロードキャスト
class Message < ApplicationRecord
belongs_to :room
belongs_to :user
after_create_commit do
ChatChannel.broadcast_to(room, { user: user.name, body: body })
end
end
broadcast_to(room, ...) は、stream_for room で購読した全員に届きます。配信名を文字列で決めたいなら、購読側を stream_from "chat_#{room.id}"、送信側を ActionCable.server.broadcast("chat_#{room.id}", ...) にします。after_create ではなく after_create_commit にするのは、トランザクションがロールバックされたメッセージを配信しないためです。配信処理が重いなら Rails Active Jobとは?非同期ジョブ処理の実装とSolid Queue・リトライ設定のジョブへ移します。
接続時の認証と許可するオリジン
コネクションでのユーザー特定
Rails 8.1の認証ジェネレーターは、app/channels/application_cable/connection.rb を次の内容で生成します(v8.1.4のテンプレートから引用)。
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
set_current_user || reject_unauthorized_connection
end
private
def set_current_user
if session = Session.find_by(id: cookies.signed[:session_id])
self.current_user = session.user
end
end
end
end
identified_by :current_user を宣言すると、全チャネルで current_user が使えます。Railsガイドは、WebSocketサーバーからはセッションに触れられないがCookieは読める、と注意しています。そのため、ログイン時に署名付きCookieや暗号化Cookieへ識別子を入れておき、ここで照合します。照合できなければ reject_unauthorized_connection で接続自体を断ります。
allowed_request_originsとCSRF対策
Action Cableは、許可したOriginからの接続だけを受け付けます。開発環境では /https?:\/\/localhost:\d+/ が既定値で、localhostならポートを問わず許可されます(Railsガイドの「localhost:3000」は代表例です)。本番でも、ページと同じOrigin(Host ヘッダーと一致するOrigin)からの接続は allow_same_origin_as_host が既定で true のため許可されます。フロントエンドを別ドメインに置くときに、そのOriginを列挙します。
# config/environments/production.rb
config.action_cable.allowed_request_origins = ["https://example.com", "https://chat.example.com"]
正規表現も渡せますが、%r{https://.*\.example\.com} のように末尾を固定しないと https://chat.example.com.attacker.test にも一致します。上の例のように完全一致の文字列で書く方が安全です。config.action_cable.disable_request_forgery_protection = true で全Originを許可できますが、Cookie認証と組み合わせると、別サイトのページからログイン中ユーザーとして接続されます(Cross-Site WebSocket Hijacking)。フロントエンドを別ドメインに置くなら、無効化せずにそのドメインを列挙してください。
サブスクリプションアダプタの選び方
Pumaのワーカーやサーバーを複数台にすると、あるプロセスで呼んだ broadcast を別プロセスにつながったブラウザへ届ける中継役が要ります。それがサブスクリプションアダプタで、config/cable.yml で環境ごとに選びます。
| アダプタ | 中継の方式 | 向く環境 | 制約 |
|---|---|---|---|
| async | プロセス内のメモリ | 開発 | 別プロセスへ届かない |
| test | 記録+プロセス内配信 | テスト | asyncを継承 |
| solid_cable | DBテーブルをポーリング | Rails 8の本番既定 | 既定0.1秒間隔で遅延 |
| redis | Redis Pub/Sub | 既存のRedisがある構成 | Redisの運用が要る |
| postgresql | LISTEN/NOTIFY | PostgreSQLのみの構成 | 既定で8000バイト未満 |
選ぶ順は、Rails 8で新規作成したならSolid Cableのまま、Redisを既に運用しているならRedis、でよいでしょう。PostgreSQLアダプタは下の制約を理解したうえで選びます。
Solid Cable(Rails 8の既定)
Solid CableはメッセージをDBテーブルに書き、各サーバーが一定間隔で読みに行く方式です。Solid CableのREADMEには、Rails 8の新規アプリでは既定で設定済みとあり、MySQL・SQLite・PostgreSQLで動作確認されています。solid_cable:install が作る設定(v4.1.0)は次のとおりです。
production:
adapter: solid_cable
connects_to:
database:
writing: cable
polling_interval: 0.1.seconds
message_retention: 1.day
writer_batch_size: 4
writer_batch_delay: 0.001.seconds
0.1秒は各サーバーがDBを読みに行く間隔で、配信の遅れはこれにDBの処理時間と負荷が加わります。メッセージは保持期間(既定1日)を過ぎると自動トリミングの削除対象になり、メッセージを1件書くたびに、期限切れの行を2件ずつ削除しようとする方式です。config/database.yml に cable という名前のDB設定を足す作業は手動です。2026年9月24日公開の4.1.0では書き込みのバッチ化と、encrypt: true によるActive Record Encryptionでの暗号化が加わりました。暗号化を使う場合、既定のMessagePackシリアライザのために msgpack gemも必要です。
Redisの採用条件と接続設定
rails new --skip-solid で作ると、production は adapter: redis と channel_prefix: アプリ名_production で生成されます。1台のRedisを複数アプリで共有するときに配信名が衝突しないよう、channel_prefix は消さないでください。TLS接続は rediss:// と ssl_params で指定します。8.1.4でredis-rb 6系が使えるようになりました。Redis互換のValkeyとは?Redisとの違い・Streamsの使い方と導入手順【Valkey 9.1】を接続先にする構成もあります。
PostgreSQLと日本語の配信名
PostgreSQLアダプタは、アプリの database.yml の接続を流用して NOTIFY で配信します。Railsガイドが注記しているとおり、NOTIFY のペイロードは、既定設定では8000バイト未満でなければなりません。判定対象はJSON化などを終えた送信ペイロード全体です。本文の長いメッセージや画像データを流すと失敗するので、IDだけ送ってブラウザ側で取りに行かせます。
もう1つの落とし穴が配信名の長さです。PostgreSQLの識別子は63バイトまでで、超えた部分は黙って切り捨てられます。アダプタは長い配信名をSHA1に置き換えて回避していますが、8.1.3までは判定を文字数で行っていました。日本語はUTF-8で1文字3バイトなので、"room_" に日本語21文字を続けた名前は26文字でも68バイトになります。判定をすり抜けて切り捨てられ、購読側と配信側の名前が食い違って届かなくなるわけです。8.1.4のCHANGELOGはこの修正を「長いマルチバイトの配信名へのブロードキャストが落ちる」問題として記載しています。stream_from "room_#{room.name}" のように利用者の入力を配信名に入れている場合は、8.1.4に上げるか、名前をIDに置き換えてください。
切断の検知と再接続の仕組み
「しばらく放置すると届かなくなる」「スマホで戻ると古い画面のまま」という相談は、クライアントの再接続の挙動を知ると切り分けられます。v8.1.4のconnection_monitor.jsなどソースでの値は次のとおりです。
- サーバーは全接続へ3秒ごとに
pingを送る(BEAT_INTERVAL = 3) - クライアントは定期的な監視のたびに、pingを含む最後のメッセージ受信から6秒(
staleThreshold = 6、ping2回分)を超えていれば接続が古いと判断し、張り直す - 監視の間隔は6秒を基準に、再接続の試行が続くたび1.15倍ずつ伸びる(10回分で頭打ち)。ランダムなばらつきを加えて、全クライアントが同時に再接続しないようにしている
- タブが非表示から表示に戻ると、0.2秒後に接続を確認し、古ければ即座に張り直す
pingが3秒ごとに流れるので、ロードバランサーやリバースプロキシのアイドルタイムアウトが数秒以上あれば、通常は無通信で切られることはありません。切り分けは、接続できた後に切れるのか、最初から接続できないのかで分けます。接続後に切れるなら、サーバーの負荷でpingが遅れていないか、端末がスリープしていないか、プロキシのタイムアウトを確認します。最初から接続できないなら、プロキシがUpgradeヘッダーを転送し、101 Switching Protocolsが返っているかを確認します。もう1つ重要なのは、切断中に配信されたメッセージは再接続後に再送されないことです。取りこぼしが許されない画面では、connected() コールバックで最新状態をHTTPで取り直す処理を入れます。原因を追うときは、JavaScriptモジュールで import * as ActionCable from "@rails/actioncable" を追加し、ActionCable.logger.enabled = true にすると接続の監視ログが出ます。
本番運用:ワーカープール・スタンドアロン構成・スケール
ワーカープールとDB接続数
チャネルのアクションやコネクションのコールバックは、Action Cable専用のワーカープールで実行されます。既定は config.action_cable.worker_pool_size = 4 です。Railsガイドは、DB接続数をワーカー数以上にするよう求めています。database.yml の pool がPumaのスレッド数ぎりぎりだと、チャネル内のDBアクセスで接続待ちが起きます。Solid Cableを使うなら、cable 用DBの接続も別枠で数えてください。
アプリ内マウントとスタンドアロンサーバー
既定ではRailsアプリの /cable にマウントされ、同じPumaで動きます。Railsガイドは開発ではこれで十分としつつ、本番ではスタンドアロンで動かすよう勧めています。スタンドアロンは次の cable/config.ru を別のPumaで起動します。
# cable/config.ru
require_relative "../config/environment"
Rails.application.eager_load!
run ActionCable.server
起動は bundle exec puma -p 28080 cable/config.ru で、アプリ側は config.action_cable.mount_path = nil にしてマウントを外し、config.action_cable.url に接続先(本番は wss://)を書きます。ブラウザ側は、レイアウトのheadに action_cable_meta_tag を置くと引数なしの createConsumer() がそのURLへ接続し、置かない場合は createConsumer("wss://cable.example.com") のように引数で渡します。分けると、長くつながり続けるWebSocketと通常のリクエストがPumaのスレッドを奪い合わなくなります(同じホストのCPU・メモリや共有DBの競合は残ります)。接続はRackのソケットhijack APIで引き取っており、Rack側の仕組みはRackとは?Rubyのサーバーとアプリをつなぐ仕様とミドルウェアの書き方【Rack 3.2】にまとめています。
同時接続が増えたときのAnyCable
Action Cableは接続ごとにRubyプロセス内のメモリとスレッド資源を使うため、同時接続が増えるほどサーバー台数がかさみます。その段階で検討されるのがAnyCable(anycable-rails 1.6.2)で、WebSocketの接続維持をGo製のサーバーに任せ、チャネルのRubyコードは基本的に流用できます。ただしAnyCableの公式ドキュメントは、チャネルのインスタンス変数に状態を持たせることに依存できないとしており、本記事の @room のような箇所は state_attr_accessor への書き換えが要ります。periodically によるタイマーとカスタムのstreamコールバックは非対応です。Railsガイドに同時接続数の上限値は書かれていないので、切り替えの判断は自社の負荷試験で決めてください。
テスト:コネクション・チャネル・ブロードキャストの検証
Railsには ActionCable::Connection::TestCase と ActionCable::Channel::TestCase が用意されていて、test環境の adapter: test は配信内容を記録するので、何が送られたかをアサーションで確かめられます(testアダプタはasyncを継承しており、同じプロセス内への配信もします)。
class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
test "セッションが無ければ接続を拒否する" do
assert_reject_connection { connect }
end
test "ログイン済みなら current_user が入る" do
session = users(:one).sessions.create!
cookies.signed[:session_id] = session.id
connect
assert_equal users(:one), connection.current_user
end
end
class ChatChannelTest < ActionCable::Channel::TestCase
test "所属ルームを購読できる" do
stub_connection current_user: users(:one)
subscribe room_id: rooms(:one).id
assert subscription.confirmed?
assert_has_stream_for rooms(:one)
end
test "所属していないルームは拒否する" do
stub_connection current_user: users(:one)
subscribe room_id: rooms(:other).id
assert subscription.rejected?
end
end
モデルやジョブからの配信は、ActionCable::TestHelper の assert_broadcast_on(ChatChannel.broadcasting_for(room), body: "こんにちは") { ... } で確認します。broadcast_to の配信名は broadcasting_for で作ると、GlobalIDの形式を手で書かずに済みます。上のテストと同じ構成は、Ruby 4.0.6とactioncable 8.1.4で、DBの代わりに簡易なモデルを置いて4テストが通ることを確認済みです(DB保存・after_create_commit・ブラウザ側は検証範囲外)。
Action Cableの直接実装を避ける場面と代替手段
Action Cableは双方向通信のための仕組みです。次の条件に当てはまるなら、別の手段の方が構成も運用も軽くなります。
- サーバーから一方向に流すだけ:進捗表示やLLMの逐次出力ならSSE(Server-Sent Events)で足り、HTTPのまま通せる。比較はSSEとWebSocketの違いと使い分け|WebRTC・WebTransportまで比較【2026年9月】
- Hotwire構成の画面更新:Action Cableの上でTurbo Streamsを使えば、
receivedのJavaScriptを書かずにHTML断片で更新できる。turbo_stream_fromとbroadcast_append_toの使い方はTurboとは?Hotwireの高速化フレームワークの仕組みとRailsでの使い方【Turbo 8.0.23】 - 数十秒単位の更新で足りる:ダッシュボードの集計値などは定期ポーリングの方が、接続維持の資源を使わない
- 大きなデータを流す:PostgreSQLアダプタのペイロードは、既定設定では8000バイト未満。Solid CableもDBに書くので、ファイルは別経路で渡してIDだけ配信する
操作を即座にほかのユーザーへ反映するチャット・共同編集・対戦系の画面は、Action Cableの守備範囲です。
Action Cableに関するよくある質問
ActionCableとAction Cableは違うものですか?
同じものです。公式の表記は「Action Cable」、gem名とRubyのモジュール名は actioncable/ActionCable、npmパッケージは @rails/actioncable です。npm側の版番号は8.1.400のように、Railsの8.1.4を3桁で表した形式です。
Action CableにRedisは必須ですか?
必須ではありません。Rails 8の新規アプリではSolid CableがDBを使って中継するため、Redisなしで本番運用できます。Redisは既存の運用資産がある場合や、ポーリングによる0.1秒前後の遅延も避けたい場合に選びます。
rails consoleからbroadcastしてもブラウザに届かないのはなぜですか?
開発環境の async アダプタが、同じプロセスの中でしか配信を中継しないためです。bin/rails console は別プロセスなので、サーバーにつながったブラウザへは届きません。Railsガイドは、開発サーバーのプロセス内で動くweb-consoleから実行するよう案内しています。開発でもRedisかSolid Cableに切り替えれば、コンソールからも届きます。
Action CableとTurbo Streamsの違いは何ですか?
Turbo StreamsはHTML断片と操作(append・replaceなど)を送って画面を更新する仕組みで、HTTPのレスポンスやSSEでも届けられます。Railsの turbo_stream_from でリアルタイムに配信するときの経路がAction Cableで、受信側のJavaScriptをほぼ書かずに済みます。
Action CableはAPIモードのRailsやReactからも使えますか?
使えます。@rails/actioncable をnpmで入れて createConsumer("wss://api.example.com/cable") のように接続先を渡します。フロントエンドのドメインは allowed_request_origins に加え、認証はCookieか接続URLのパラメータで行います。APIモードの構成はRails APIとは?作成手順・JWT認証・React連携・Docker環境を実装解説【Rails 8.1】を参照してください。