ActionCable WebSockets in Rails
ActionCable WebSockets in Rails give you a native way to add real-time updates, live notifications, and fast collaborative experiences to modern web applications. Users expect instant updates, and ActionCable is Rails’ native solution for adding WebSocket functionality to your applications while keeping the API inside the Rails ecosystem.
Intro Context
ActionCable fits best when the browser needs to hear about changes right away instead of waiting for the next refresh. Chat rooms, dashboard counters, status updates, and collaborative editors all benefit from a persistent connection. Because ActionCable stays inside the Rails conventions you already know, you can combine WebSockets with authentication, models, and partial rendering without adding a separate realtime stack.
If you want the official reference while you read, the Rails guide on Action Cable is the canonical overview. This tutorial focuses on the practical pieces: how messages move, where to configure the connection, and how to keep the code maintainable once the feature grows.
TL;DR
- Use ActionCable when your UI should update immediately after an event happens.
- Configure the cable endpoint and adapter first, then create channels for each stream of messages.
- Keep authorization at both the connection layer and the channel layer.
- Use Redis in production, because the async adapter is only suitable for local development.
What is ActionCable?
ActionCable is a framework for handling WebSocket connections in Rails applications. It combines WebSockets with the rest of your Rails app, allowing you to stream data to connected clients in real-time while leveraging your existing authentication, authorization, and business logic.
WebSockets provide a persistent connection between the client and server, unlike traditional HTTP requests where the client must constantly poll the server for updates. This bidirectional communication channel enables:
- Real-time updates without page refreshes
- Live notifications
- Collaborative editing features
- Live chat applications
- Real-time dashboards and monitoring
Setting up ActionCable
ActionCable comes bundled with Rails 5 and later. To enable it, you need to configure both the server-side and client-side components.
server-side configuration
First, ensure your Rails application has the ActionCable engine mounted. The mount directive in the routes file tells Rails to accept WebSocket upgrade requests at the /cable path and hand them off to ActionCable’s connection handler. This is the single server-side entry point for all real-time traffic:
Rails.application.routes.draw do
mount ActionCable.server => '/cable'
# Your other routes...
end
Then configure the cable settings in config/cable.yml:
development:
adapter: async
test:
adapter: async
production:
adapter: redis
url: redis://localhost:6379/1
channel_prefix: rails_actioncable_production
The Redis adapter is recommended for production as it supports multiple server instances and handles connections across a cluster.
Redis matters because each Rails worker can share the same pub/sub backend. Without that shared broker, one process would not know what another process broadcast, which is why the async adapter is fine locally but not enough in production.
client-side setup
In your JavaScript, initialize the ActionCable consumer:
import { createConsumer } from "@rails/actioncable"
const cable = createConsumer("/cable")
This creates a WebSocket connection to the /cable endpoint.
That consumer object is the JavaScript half of the bridge. It keeps the websocket alive, handles reconnects, and gives you a place to define callbacks for connect, disconnect, and received data.
Channels: the building blocks
Channels are the core abstraction in ActionCable. They define how messages are routed between the server and connected clients.
creating a channel
Generate a new channel using Rails generators:
rails generate channel chat
This creates two files:
app/channels/chat_channel.rb(server-side)app/javascript/channels/chat_channel.js(client-side)
The generator gives you a matching server file and client file, which keeps the subscription name and the browser-side consumer in sync. That symmetry matters, because ActionCable works best when the message name and the stream name stay easy to trace.
server-side channel
Define your channel class:
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room_id]}"
end
def unsubscribed
# Clean up any resources
end
def send_message(data)
Message.create!(
content: data['message'],
room_id: params[:room_id],
user_id: current_user.id
)
end
end
The subscribed method is called when a client subscribes to the channel. Use stream_from for broadcast messaging or stream_for for user-specific streams.
The server-side channel is where you decide what each subscriber is allowed to hear. It is also where you connect incoming payloads to model changes, background jobs, or any other Rails code that should run after a message arrives.
client-side channel
Handle the connection on the client:
import consumer from "./consumer"
consumer.subscriptions.create("ChatChannel", {
connected() {
console.log("Connected to chat")
},
disconnected() {
console.log("Disconnected from chat")
},
received(data) {
// Handle incoming messages
this.appendMessage(data)
},
send_message(message) {
return this.perform("send_message", { message })
},
appendMessage(data) {
const messagesContainer = document.getElementById("messages")
messagesContainer.insertAdjacentHTML("beforeend", `
<div class="message">
<strong>${data.user}:</strong> ${data.content}
</div>
`)
}
})
The client-side channel turns incoming websocket traffic into browser behavior. You can update the DOM, render notifications, or hand the payload off to a framework like Stimulus or Turbo.
Broadcasting messages
From anywhere in your Rails application, you can broadcast messages to connected clients:
ActionCable.server.broadcast("chat_#{room.id}", {
user: message.user.name,
content: message.content,
timestamp: message.created_at.iso8601
})
This sends the data to all clients subscribed to the “chat_#{room.id}” stream.
Choose between raw broadcasts and rendered partials based on where you want the presentation logic to live. Raw payloads are flexible, while partials keep the HTML in the view layer and make it easier to change later.
broadcasting from models
A common pattern is to broadcast from model callbacks:
class Message < ApplicationRecord
after_create_commit do
broadcast_append_to(
"chat_#{room_id}",
partial: "messages/message",
locals: { message: self }
)
end
end
The broadcast_append_to method automatically streams the rendered partial to subscribers.
This pattern is handy when the database record itself should trigger the UI update. It keeps the websocket code close to the model event, which makes it easier to understand than scattering broadcast calls through controllers.
Authentication and authorization
ActionCable integrates with your existing authentication system.
using Devise
In your connection authorization:
class ApplicationCable::Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
if current_user = env['warden'].user
current_user
else
reject_unauthorized_connection
end
end
end
channel authorization
Restrict channel subscriptions to authorized users:
class ChatChannel < ApplicationCable::Channel
def subscribed
if current_user.can_access_room?(params[:room_id])
stream_from "chat_#{params[:room_id]}"
else
reject
end
end
There are two auth checkpoints here. The connection decides whether the browser can open a websocket at all, while the channel decides which streams that authenticated user may join. Keeping both layers explicit makes later debugging much easier.
Performance considerations
ActionCable handles many concurrent connections, but you should follow best practices:
1. use Redis in production
The async adapter works for development but doesn’t persist connections. Use Redis for production. Add the Redis gem to your Gemfile and configure the cable adapter to point at your Redis instance so that broadcasts reach all server processes, not just the one that originated the message:
# Gemfile
gem "redis", "~> 4.0.1"
2. limit connection pool size
Configure your Redis connection pool. The max_pool_size setting controls how many simultaneous Redis connections ActionCable can open, and the timeout value determines how long it waits before giving up on a connection. Tune these based on your expected concurrent WebSocket count:
# config/cable.yml
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: myapp_production
timeout: 1
max_pool_size: 10
3. monitor connections
Use ActionCable’s built-in statistics to monitor connection and channel counts in production. The statistics method gives you a snapshot of how many clients are connected and how many channels are active, which is useful for capacity planning and spotting unusual spikes:
ActionCable.server.statistics
# => { connections: 150, channels: 230 }
Production tuning mostly comes down to keeping connection counts predictable and avoiding expensive work on every incoming message. If you can batch updates or collapse repeated events, you reduce pressure on the websocket layer and keep the app easier to scale.
4. graceful degradation
Always have a fallback for clients that don’t support WebSockets. The JavaScript snippet below checks for ActionCable support and falls back to AJAX polling on an interval, which keeps the feature working for older browsers or network environments that block WebSocket connections:
if (window.ActionCable) {
// Use WebSocket
} else {
// Poll via AJAX
setInterval(fetchMessages, 5000)
}
Advanced patterns
private channels
For user-specific streams. The stream_for method accepts a model instance and creates a named stream keyed to that record, so you can target broadcasts to a single user’s notification channel without leaking messages to other connected clients:
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end
# Broadcast to a specific user
NotificationsChannel.broadcast_to(
current_user,
{ type: "notification", message: "New activity!" }
)
presence
Track who’s online:
class ChatChannel < ApplicationCable::Channel
def subscribed
@room = Room.find(params[:room_id])
presence = {
current_user: {
id: current_user.id,
name: current_user.name,
avatar: current_user.avatar_url
}
}
stream_for @room
appear(presence)
end
end
Presence tracking is usually best treated as a separate concern from the main chat payloads. That keeps the message stream simple and lets you update online state, avatars, or typing indicators independently from the content itself.
Testing ActionCable
ActionCable includes test helpers for both unit and integration tests.
channel tests
require "test_helper"
class ChatChannelTest < ActionCable::Channel::TestCase
test "subscribes to room stream" do
stub_connection params: { room_id: 1 }
subscribe
assert subscription.confirmed?
assert_has_stream "chat_1"
end
end
integration tests
require "test_helper"
class ChatIntegrationTest < ActionDispatch::IntegrationTest
test "messages are broadcasted" do
# Set up a WebSocket connection
cable = ActionCable.create_connection
# Subscribe to channel
cable.subscriptions.create "ChatChannel", room_id: 1
# Perform action that creates a message
post "/messages", params: { message: "Hello", room_id: 1 }
# Verify broadcast was sent
assert_broadcast "chat_1", { content: "Hello" }
end
end
Tests are where you verify both halves of the connection: that a subscription opens successfully and that a broadcast reaches the right stream. If you only test the broadcast, it is easy to miss a channel authorization bug.
Common issues and solutions
Connection Drops
If connections drop frequently, check:
- Network stability
- Redis connectivity
- Server resource usage (memory, CPU)
Memory Leaks
Prevent memory leaks by:
- Cleaning up subscriptions in
unsubscribed - Closing connections properly
- Monitoring Redis memory usage
Cross-Origin Connections
For cross-domain WebSocket connections, configure allowed origins:
# config/environments/production.rb
config.action_cable.allowed_request_origins = [
"https://yourdomain.com",
/https:\/\/.*\.yourdomain\.com/
]
Forward Link
Once the websocket path works, the next thing to review is how the rest of the Rails app feeds it data. Many projects pair ActionCable with background jobs, Turbo Streams, or model callbacks so that new events reach the browser without extra controller logic. That next layer is where the feature starts to feel polished, because the transport is already in place.
summary
ActionCable is the part of Rails that keeps browsers and servers talking in real time. Use it when polling is too slow, keep the channel logic narrow, and remember that authorization happens both when the connection opens and when a subscription starts. In production, Redis and careful connection management matter more than the exact shape of the JavaScript consumer.
ActionCable makes adding real-time features to Rails applications straightforward. By using WebSockets and integrating with Rails’ existing architecture, you can build interactive features that keep users updated without managing a separate realtime service.