ActionCable and WebSockets in Rails
Real-time features have become essential for modern web applications. Users expect instant updates, live notifications, and seamless collaborative experiences. ActionCable is Rails’ native solution for adding WebSocket functionality to your applications, providing a unified API that integrates smoothly with the Rails ecosystem.
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. In config/routes.rb:
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.
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.
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)
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.
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>
`)
}
})
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.
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.
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
end
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:
# Gemfile
gem "redis", "~> 4.0.1"
2. Limit Connection Pool Size
Configure your Redis connection pool:
# 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:
ActionCable.server.statistics
# => { connections: 150, channels: 230 }
4. Graceful Degradation
Always have a fallback for clients that don’t support WebSockets:
if (window.ActionCable) {
// Use WebSocket
} else {
// Poll via AJAX
setInterval(fetchMessages, 5000)
}
Advanced Patterns
Private Channels
For user-specific streams:
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
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
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/
]
ActionCable makes adding real-time features to Rails applications straightforward. By leveraging the power of WebSockets and integrating seamlessly with Rails’ existing architecture, you can build interactive features that keep users engaged without the complexity of managing separate real-time services.