ActionCable in Rails – Real-Time Features, Behind-the-Scenes & Production Tips
Learn how ActionCable adds real-time WebSocket support to your Rails app. We’ll cover architecture, how it works, authentication, Redis, scaling, use cases, and best practices.
What is ActionCable?
ActionCable is Rails’ built-in WebSocket framework that allows real-time features like chat, live updates, notifications, and collaborative tools, tightly integrated with Rails conventions.
Why Use ActionCable?
Reason | Description |
---|---|
Real-Time Communication Without External Services | ActionCable allows you to implement real-time WebSocket features natively in Rails, avoiding third-party services like Pusher or Firebase. This reduces external dependencies and cost. |
Rails Convention Support (Channels) | It uses Rails conventions like channels, which are similar to controllers, making it easy for developers to stay within familiar MVC patterns when building real-time features. |
Built-in Authentication & Redis Scaling | WebSocket connections are authenticated using sessions or tokens. Redis is used in production to synchronize connections across multiple servers for scaling. |
Frontend Integration | ActionCable integrates with Rails frontend tools like StimulusJS and Turbo, allowing you to easily subscribe to channels and receive updates on the client side with minimal setup. |
Key ActionCable Terms – Description & Implementation
This section breaks down every important term used in ActionCable, what it means, and how it’s used in code.
/cable
Description: The WebSocket endpoint mounted in routes.rb
that the frontend connects to.
Implementation:
# config/routes.rb
mount ActionCable.server => '/cable'
Connection
Description: Handles authentication and identifies users. Located at app/channels/application_cable/connection.rb
.
Implementation:
class ApplicationCable::Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = User.find_by(id: cookies.encrypted[:user_id]) || reject_unauthorized_connection
end
end
Channel
Description: The WebSocket equivalent of a controller. Channels define what to stream and how to handle incoming messages.
Implementation:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_channel"
end
end
stream_from
Description: Subscribes a user to a specific Redis stream name. When this stream is broadcasted to, the client receives data.
Implementation:
stream_from "chat_channel"
stream_for
Description: Shortcut for user-specific or object-specific streams. Automatically generates a unique Redis stream name.
Implementation:
stream_for current_user
NotificationsChannel.broadcast_to(current_user, { alert: "Hello!" })
ActionCable.server.broadcast
Description: Sends a message to a stream (channel). All users subscribed to this stream receive the message.
Implementation:
ActionCable.server.broadcast("chat_channel", { message: "Hi" })
received
(Frontend)
Description: The frontend function that runs whenever a broadcast is received.
Implementation:
consumer.subscriptions.create("ChatChannel", {
received(data) {
console.log(data.message);
}
});
perform
Description: The frontend uses perform
to send data to the server (calls methods defined in the channel).
Implementation:
# JavaScript
subscription.perform("speak", { message: "Hi from client" })
# Ruby (ChatChannel)
def speak(data)
ActionCable.server.broadcast("chat_channel", { message: data["message"] })
end
unsubscribed
Description: Called when a user disconnects. Useful for cleaning up, marking them offline, etc.
Implementation:
def unsubscribed
Rails.logger.info "User disconnected"
end
Redis
(Adapter)
Description: Backend message broker that powers broadcasting in production. It ensures all ActionCable servers receive the same messages.
Implementation:
# config/cable.yml
production:
adapter: redis
url: redis://localhost:6379/1
How It Works – Behind the Scenes
When a user visits your app, the frontend JavaScript creates a WebSocket connection to the path /cable
. This connection allows real-time, bidirectional communication between the server and the browser, unlike traditional HTTP requests.
🔌 Step-by-Step Breakdown
-
1. WebSocket Request to
/cable
:
The frontend sends a WebSocket handshake request to/cable
. This connection is long-lived and allows real-time communication without the need to refresh the page or poll the server repeatedly. -
2. Connection Authenticated in
ApplicationCable::Connection
:
Rails authenticates the connection inapp/channels/application_cable/connection.rb
. You can extract the current user based on session, cookies, or a token.
Example:identified_by :current_user def connect self.current_user = find_verified_user end
-
3. Subscription to a Channel:
After connection, the client sends a subscription request to a specific channel (likeChatChannel
orNotificationChannel
). Channels are similar to controllers but for WebSocket streams. -
4. Stream Setup with Redis:
When the server callsstream_from
orstream_for
, it sets up a Redis pub/sub stream. Any broadcast to that stream is forwarded to all ActionCable instances (servers or containers). -
5. Server Broadcasts Messages:
From anywhere in your Rails app, you can send data using:
This data is published to Redis, and all connected clients subscribed toActionCable.server.broadcast(\"chat_room_1\", { message: \"Hello\" })
chat_room_1
will receive it. -
6. Client Receives Data Instantly:
The frontend JavaScript receives the data via the open WebSocket connection and updates the DOM or performs any action (like appending a new chat message).
📶 Data Flow Diagram
Browser ➡ WebSocket ➡ NGINX ➡ Rails ➡ ActionCable ➡ Redis ➡ Other Clients
🧠 Key Components in ActionCable Flow
Component | Role |
---|---|
/cable |
WebSocket endpoint mounted in routes.rb |
Connection |
Handles authentication and identifies users |
Channel |
Handles subscription logic and defines streams |
Redis |
Pub/Sub layer for broadcasting to all ActionCable servers |
Frontend | Receives data via WebSocket and updates the UI |
🧩 Example Use Case
Imagine a user sends a message in a chat:
- User types “Hello” and clicks send.
- The message is sent to the backend via WebSocket.
- Rails broadcasts it using
ActionCable.server.broadcast
. - Redis distributes the message to all servers (even if horizontally scaled).
- All clients in the same chat room receive the message and update their UI instantly.
Basic Implementation
This example shows how to set up ActionCable to send and receive real-time messages using WebSockets in a Rails app. We’ll go step-by-step with clean and simple code.
📦 Step 1: Generate a Channel
rails generate channel Chat
This will create:
app/channels/chat_channel.rb
app/javascript/channels/chat_channel.js
🧠 Step 2: Channel Backend Code (No Room, No Params)
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from \"chat_channel\"
end
end
Explanation:
subscribed
is called when the frontend connects.stream_from \"chat_channel\"
sets up a named stream.
🛣️ Step 3: Mount ActionCable
# config/routes.rb
mount ActionCable.server => \"/cable\"
This makes the WebSocket connection available at /cable
.
🧪 Step 4: Add Redis for Production (Optional in Dev)
# config/cable.yml
development:
adapter: async
production:
adapter: redis
url: redis://localhost:6379/1
📺 Step 5: JavaScript – Subscribe and Receive Messages
// app/javascript/channels/chat_channel.js
import consumer from \"./consumer\"
consumer.subscriptions.create(\"ChatChannel\", {
connected() {
console.log(\"Connected to chat_channel\");
},
received(data) {
const box = document.getElementById(\"chat-box\");
box.innerHTML += `<p>\${data.message}</p>`;
}
});
Explanation:
connected
runs when WebSocket connects.received
is triggered when the server broadcasts data.
📤 Step 6: Broadcasting from Server (Anywhere in Rails)
ActionCable.server.broadcast(\"chat_channel\", { message: \"Hello, world!\" })
You can trigger this in a controller action, a background job, or a console to test it:
rails c
ActionCable.server.broadcast(\"chat_channel\", { message: \"It works!\" })
🧾 Step 7: Add HTML to View (to Display Messages)
<div id=\"chat-box\"></div>
10 Real-World Use Cases
- Chat systems
- Notifications (e.g., “new comment”)
- Typing indicators
- Real-time dashboards
- Collaborative editing (e.g., Docs)
- Multiplayer game lobbies
- Live polls or voting
- Trading or price updates
- Auction countdowns
- Order status/live tracking
Technical Q&A
ActionCable uses Redis
as a pub/sub system to distribute messages between multiple Rails server instances.
This allows one server to broadcast a message, and all others can deliver it to their connected clients.
Example:
# config/cable.yml
production:
adapter: redis
url: redis://localhost:6379/1
Use stream_for current_user
in the channel to stream only to that user, and broadcast_to
to send them messages.
Example:
# Channel
stream_for current_user
# Somewhere in Rails
NotificationsChannel.broadcast_to(current_user, { alert: "You have a new message" })
Authentication happens in ApplicationCable::Connection
inside the connect
method.
You typically use session cookies or tokens to find and authorize the user.
# app/channels/application_cable/connection.rb
identified_by :current_user
def connect
self.current_user = User.find_by(id: cookies.encrypted[:user_id]) || reject_unauthorized_connection
end
The unsubscribed
method is automatically called when a client disconnects from a channel.
You can use this to remove online indicators or clean up streams.
def unsubscribed
Rails.logger.info \"User left the room\"
end
Yes. ActionCable works with Turbo Streams and StimulusJS. You can trigger Turbo updates on WebSocket messages.
<turbo-stream action=\"append\" target=\"messages\">
<template>
<div>New message</div>
</template>
</turbo-stream>
In the JavaScript client, use consumer.subscriptions.create
to connect and define how to handle messages.
consumer.subscriptions.create("ChatChannel", {
received(data) {
console.log(data.message);
}
});
ActionCable sends JSON messages. The backend and frontend communicate using JSON-formatted payloads.
{ message: "Hello World", user: "Alice" }
Yes. You can define public methods in your channel and call them from the frontend using perform()
.
# Ruby
def speak(data)
ActionCable.server.broadcast("chat_channel", { message: data["message"] })
end
# JS
subscription.perform("speak", { message: "Hi from client!" });
In production, Redis is required for ActionCable to sync. If Redis is down, messages will not be delivered to clients connected to other servers. Always use a highly available Redis instance or managed Redis (e.g., AWS Elasticache).
Use RSpec with type: :channel
to test subscription and stream behavior.
RSpec.describe ChatChannel, type: :channel do
it "subscribes to a stream" do
stub_connection current_user: create(:user)
subscribe
expect(subscription).to be_confirmed
expect(subscription).to have_stream_from("chat_channel")
end
end
Production Best Practices
config/cable.yml
)
Redis is required in production to sync WebSocket messages across multiple server processes or containers. Without Redis, only one process can receive and send WebSocket data.
production:
adapter: redis
url: redis://localhost:6379/1
Connection
class
Do not allow anonymous WebSocket connections in production. Use cookies, sessions, or tokens to verify the user when a connection is opened.
identified_by :current_user
def connect
self.current_user = find_verified_user || reject_unauthorized_connection
end
stream_for
for scoped data
Instead of using shared stream names (like chat_channel
), use stream_for
to stream data per user or per object. This helps reduce unwanted broadcasts and improves security.
stream_for current_user
NotificationsChannel.broadcast_to(current_user, { alert: "Hi!" })
Keep your broadcast messages small. Don’t send HTML or large JSON objects. Instead, send minimal data and let the frontend handle rendering.
{ message: "New comment", id: 123 }
In large apps, run ActionCable on its own server or container. This avoids WebSocket connections competing with web traffic on the same Puma threads.
Tip: Use a subdomain like ws.yourapp.com
to isolate WebSocket traffic.
Use tools like Redis CLI or metrics dashboards to monitor memory, channels, and clients. Also monitor WebSocket connections per instance to avoid overload.
redis-cli info clients
Avoid doing database queries or external API calls directly in your channel. Use background jobs (Sidekiq, Delayed Job, etc.) and just broadcast the result.
ChatBroadcastJob.perform_later(message)
Browsers may disconnect due to network issues. Add retry logic on the frontend to reconnect automatically if the WebSocket connection drops.
disconnected() {
console.warn(\"Disconnected from channel. Reconnecting...\");
setTimeout(() => location.reload(), 3000);
}
⚙️ Production Configuration for ActionCable
This section covers everything you need to securely and efficiently deploy ActionCable in production using Redis and WebSocket best practices.
✅ 1. Use Redis as the Cable Adapter
Redis is required in production so ActionCable can synchronize broadcasts across threads and servers.
# config/cable.yml
production:
adapter: redis
url: redis://localhost:6379/1
✅ 2. Mount WebSocket Endpoint
This allows WebSocket connections via /cable
.
# config/routes.rb
mount ActionCable.server => '/cable'
✅ 3. Set Allowed Origins
Prevent cross-site WebSocket hijacking by allowing only specific domains.
# config/environments/production.rb
config.action_cable.allowed_request_origins = [
'https://yourdomain.com',
'http://yourdomain.com'
]
✅ 4. Configure Puma for Concurrent Connections
Set threads and workers for better WebSocket handling.
# config/puma.rb
workers Integer(ENV['WEB_CONCURRENCY'] || 2)
threads_count = Integer(ENV['RAILS_MAX_THREADS'] || 5)
threads threads_count, threads_count
preload_app!
✅ 5. Optional: Run ActionCable in a Separate Process
For large apps, you can isolate WebSocket handling from web requests.
# run separately
bundle exec puma -C cable_puma.rb
Or use a subdomain + NGINX:
# nginx config
location /cable {
proxy_pass http://action_cable_server:28080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
✅ 6. Enable WebSocket Headers in NGINX
Ensure WebSocket protocol upgrades are correctly forwarded.
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
✅ 7. Set Logging Tags for Debugging
Add request tracing to WebSocket logs.
# config/environments/production.rb
config.action_cable.log_tags = [ :uuid, -> request { request.remote_ip } ]
✅ 8. Optional: Suppress Forgery Warnings
If you’re using tokens instead of session cookies, you can disable forgery protection.
# config/environments/production.rb
config.action_cable.disable_request_forgery_protection = true
✅ 9. Use Background Jobs for Long Tasks
Offload heavy or slow tasks like database queries to jobs.
# app/channels/chat_channel.rb
def speak(data)
ChatBroadcastJob.perform_later(data)
end
✅ 10. Test After Deployment
Ensure everything works in production:
- Open browser console — should show “connected”
- Broadcast from
rails console
and see message live - Redis server is running and accessible
📋 Deployment Checklist
- ✅ Redis configured in
cable.yml
- ✅ WebSocket endpoint mounted
- ✅ Allowed origins defined
- ✅ Puma concurrency set
- ✅ Background jobs for slow work
- ✅ SSL and headers via NGINX
- ✅ Optional: Run ActionCable on subdomain
- ✅ Monitor Redis and connection counts
Pros and Cons
✅ Pros
ActionCable is part of Rails itself, so you don’t need to install third-party tools or learn new patterns. You can use it just like controllers and models with familiar Rails conventions.
Unlike services like Pusher or Firebase, ActionCable works entirely within your app. You control the entire real-time pipeline — no extra billing or vendor lock-in.
ActionCable is perfect for small to medium-sized apps with hundreds or a few thousand concurrent connections (e.g., live chat, dashboards, notifications).
You can easily authenticate WebSocket users using cookies or tokens. The connection layer ensures only verified users can subscribe to channels.
identified_by :current_user
def connect
self.current_user = User.find_by(id: cookies.encrypted[:user_id]) || reject_unauthorized_connection
end
❌ Cons
While Redis helps ActionCable scale horizontally, it isn’t optimized for very high traffic loads like chat apps with hundreds of thousands of users. In such cases, tools like AnyCable or a Go-based WebSocket server are better suited.
ActionCable is built into Rails. If you want to extract your real-time system into a separate service (e.g., microservice architecture), it’s hard to reuse ActionCable outside of Rails.
Each WebSocket connection is long-lived and consumes memory. Without Redis and proper worker separation, this can slow down your app over time.
Unlike services like Pusher or Ably, ActionCable does not provide usage metrics, user presence, or message history unless you build it yourself.
Summary
ActionCable is a powerful way to bring real-time WebSocket communication to your Rails apps. With Redis, authentication, and broadcasting tools built-in, it’s great for modern apps with features like chat, notifications, and collaboration. Just be sure to scale smartly in production.
Learn more about Rails setup