Action Cable and WebSockets in Rails: Complete Guide for Technical Interviews 2026
Deep dive into Action Cable and WebSockets in Ruby on Rails. Covers connections, channels, broadcasting, Solid Cable in Rails 8, scaling with Redis, and common interview questions with code examples.

Action Cable brings WebSocket support directly into Rails, enabling real-time features like chat, notifications, and live dashboards without external dependencies. For technical interviews in 2026, understanding Action Cable internals — from connection lifecycle to production scaling — separates strong candidates from the rest.
Action Cable integrates WebSockets into the Rails framework with the same conventions used for controllers and models. Rails 8 introduces Solid Cable, a database-backed adapter that eliminates the Redis requirement for pub/sub messaging.
WebSocket Protocol Fundamentals in the Rails Context
The WebSocket protocol (RFC 6455) establishes a persistent, full-duplex communication channel over a single TCP connection. Unlike HTTP request-response cycles, WebSockets maintain an open connection where both client and server can send messages at any time.
Action Cable wraps this protocol into a Rails-friendly abstraction. The server handles WebSocket upgrades at /cable, manages connection authentication, and routes messages through channels. The client-side JavaScript library creates and manages subscriptions automatically.
A typical HTTP request completes in milliseconds and closes. A WebSocket connection stays open for minutes, hours, or the entire session duration. This fundamental difference drives every architectural decision in Action Cable.
Action Cable Architecture: Connections, Channels, and Subscriptions
Action Cable follows a layered architecture with three core abstractions: connections handle authentication, channels encapsulate business logic, and subscriptions link consumers to specific channels.
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
# Cookies are available in WebSocket handshake
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
endThe connection class runs once per WebSocket handshake. Authentication happens here — not in individual channels. The identified_by declaration registers the user identity, making it available across all channel subscriptions on that connection.
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
room = ChatRoom.find(params[:room_id])
# Authorization: verify user belongs to this room
if room.members.include?(current_user)
stream_for room
else
reject
end
end
def receive(data)
message = ChatMessage.create!(
room_id: params[:room_id],
user: current_user,
body: data["body"]
)
ChatChannel.broadcast_to(
message.room,
{ id: message.id, body: message.body, user: current_user.name }
)
end
def unsubscribed
# Cleanup: mark user as offline
AppearanceTracker.mark_offline(current_user)
end
endChannels define three lifecycle callbacks: subscribed, receive, and unsubscribed. The stream_for method binds the subscription to a specific model instance, creating a namespaced stream. Broadcasting to that stream delivers messages to every connected subscriber.
Client-Side Subscriptions with JavaScript
The client-side consumer connects to the Action Cable server and manages subscriptions. Each subscription maps to a server-side channel.
import consumer from "./consumer"
const chatChannel = consumer.subscriptions.create(
{ channel: "ChatChannel", room_id: roomId },
{
connected() {
// Called when the subscription is ready
console.log("Connected to chat room", roomId)
},
disconnected() {
// Called when the subscription is closed
console.log("Disconnected from chat room")
},
received(data) {
// Called when data is broadcast to the channel
const messageList = document.getElementById("messages")
messageList.insertAdjacentHTML("beforeend",
`<div class="message"><strong>${data.user}</strong>: ${data.body}</div>`
)
},
sendMessage(body) {
// Calls ChatChannel#receive on the server
this.perform("receive", { body: body })
}
}
)The received callback fires every time the server broadcasts to the subscribed stream. The perform method sends data from client to server, invoking the corresponding channel method.
Ready to ace your Ruby on Rails interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Solid Cable in Rails 8: Database-Backed Pub/Sub
Rails 8 introduces Solid Cable as part of the "Solid Trifecta" alongside Solid Queue and Solid Cache. Solid Cable replaces Redis as the pub/sub backend by storing messages in a database table.
# config/cable.yml (Rails 8 default)
development:
adapter: solid_cable
production:
adapter: solid_cable
connects_to:
database:
writing: cable
polling_interval: 0.1.seconds
message_retention: 1.daySolid Cable works by writing each broadcast message to a database table and polling for new messages at a configurable interval. The default polling interval of 100ms provides near-real-time delivery for most applications.
The tradeoff is clear: Solid Cable eliminates an infrastructure dependency (Redis) at the cost of slightly higher latency and database load. For applications already running PostgreSQL or MySQL, this tradeoff often makes sense. For high-frequency broadcasting (thousands of messages per second), Redis remains the better choice.
# config/database.yml (Rails 8 multi-database)
production:
primary:
<<: *default
database: myapp_production
cable:
<<: *default
database: myapp_cable_production
migrations_paths: db/cable_migrateSolid Cable uses a dedicated database to avoid contention with the primary database. This separation keeps message polling from interfering with application queries.
Broadcasting Patterns and Server-Side Triggers
Broadcasting from models, jobs, and controllers covers the three most common patterns in production Rails applications.
# Broadcasting from a model callback
class Notification < ApplicationRecord
belongs_to :user
after_create_commit :broadcast_to_user
private
def broadcast_to_user
ActionCable.server.broadcast(
"notifications_#{user_id}",
{ id: id, title: title, read: false }
)
end
end
# Broadcasting from a background job
class DashboardUpdateJob < ApplicationJob
queue_as :default
def perform(dashboard_id)
dashboard = Dashboard.find(dashboard_id)
stats = dashboard.compute_stats
ActionCable.server.broadcast(
"dashboard_#{dashboard_id}",
{ stats: stats, updated_at: Time.current.iso8601 }
)
end
endModel callbacks suit simple notifications triggered by record changes. Background jobs handle heavier computations — computing dashboard stats or aggregating data — without blocking the request cycle. The broadcast itself is always lightweight: it serializes the payload and publishes to the adapter.
Turbo Streams and Action Cable Integration
Rails 8 with Hotwire uses Action Cable as the transport layer for Turbo Streams, enabling real-time DOM updates without writing custom JavaScript.
# app/models/message.rb
class Message < ApplicationRecord
belongs_to :chat_room
# Automatically broadcasts append to subscribers
broadcasts_to :chat_room
end
# app/views/chat_rooms/show.html.erb
<%= turbo_stream_from @chat_room %>
<div id="messages">
<%= render @chat_room.messages %>
</div>The broadcasts_to macro generates after_create, after_update, and after_destroy callbacks that broadcast Turbo Stream fragments over Action Cable. The turbo_stream_from helper on the client subscribes to the matching stream. Creating a new message automatically appends its rendered partial to every connected client's DOM.
This pattern reduces real-time features to a model declaration and a view helper — no custom channels, no JavaScript handlers, no manual DOM manipulation.
Scaling Action Cable in Production
Production deployments require careful consideration of adapter choice, connection limits, and horizontal scaling.
# config/cable.yml — Redis adapter for high-scale production
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: myapp_productionRedis acts as the pub/sub backbone, ensuring messages published on one Rails process reach subscribers connected to any other process. Without Redis (or Solid Cable), the async adapter limits broadcasting to a single process — unusable in any multi-process or multi-server deployment.
Connection limits depend on the server. Puma with Action Cable handles WebSocket connections on the same process as HTTP requests. Each WebSocket holds a persistent connection, consuming a thread (or fiber in Ruby 3.x with fiber-based schedulers). A typical Puma configuration with 5 threads and 4 workers can handle roughly 200 concurrent WebSocket connections before saturation.
For applications requiring thousands of concurrent connections, AnyCable replaces the Ruby WebSocket server with a Go-based server, handling connections at the protocol level while routing channel logic back to Rails via gRPC. This architecture supports 10,000+ concurrent connections per node.
Interviewers frequently ask about Action Cable scaling limits. The key answer: Action Cable itself is not the bottleneck — the Ruby process handling WebSocket connections is. AnyCable solves this by offloading connection management to Go while keeping channel logic in Ruby.
Testing Action Cable Channels
Rails provides ActionCable::Channel::TestCase for unit testing channels and ActionCable::Connection::TestCase for connection authentication.
# test/channels/chat_channel_test.rb
require "test_helper"
class ChatChannelTest < ActionCable::Channel::TestCase
test "subscribes to a valid room" do
room = chat_rooms(:general)
user = users(:alice)
stub_connection current_user: user
subscribe room_id: room.id
assert subscription.confirmed?
assert_has_stream_for room
end
test "rejects subscription for unauthorized user" do
room = chat_rooms(:private)
user = users(:outsider)
stub_connection current_user: user
subscribe room_id: room.id
assert subscription.rejected?
end
test "broadcasts messages to room subscribers" do
room = chat_rooms(:general)
stub_connection current_user: users(:alice)
subscribe room_id: room.id
assert_broadcast_on(room, hash_including(body: "Hello")) do
perform :receive, body: "Hello"
end
end
endThe stub_connection method sets up the connection context without a real WebSocket. assert_has_stream_for verifies stream binding. assert_broadcast_on captures broadcasts within a block, confirming the correct payload reaches the correct stream.
Common Interview Questions on Action Cable
Technical interviews on Action Cable typically probe understanding of the protocol, architecture decisions, and production concerns.
How does Action Cable authenticate connections? Authentication happens in ApplicationCable::Connection#connect during the WebSocket handshake. Cookies from the HTTP session are available at this point. Token-based auth passes the token as a query parameter on the WebSocket URL and validates it in connect.
What happens when a WebSocket connection drops? The client library implements automatic reconnection with exponential backoff. On the server, unsubscribed fires for each channel the connection was subscribed to. Stateful cleanup (marking users offline, releasing locks) belongs in unsubscribed.
When should Solid Cable be used over Redis? Solid Cable suits applications with moderate real-time needs (under 100 messages/second) that want to avoid Redis infrastructure. Redis remains necessary for high-throughput scenarios or when sub-10ms delivery latency matters.
Placing authorization logic in the connection class instead of individual channels. The connection authenticates identity (who is this user?). Channels authorize access (can this user access this room?). Mixing these responsibilities creates security gaps.
Conclusion
- Action Cable wraps the WebSocket protocol into Rails conventions with connections, channels, and subscriptions as core abstractions
- Authentication belongs in
ApplicationCable::Connection; authorization belongs in individual channelsubscribedcallbacks - Solid Cable in Rails 8 eliminates Redis dependency by using the database for pub/sub — suitable for moderate-throughput applications
- Turbo Streams leverage Action Cable for automatic real-time DOM updates with minimal code
- Production scaling requires Redis or Solid Cable for multi-process deployments; AnyCable handles 10,000+ connections by offloading WebSocket management to Go
- Testing channels uses
stub_connection,assert_has_stream_for, andassert_broadcast_on— no real WebSocket connections needed - Practice ActionCable & WebSockets interview questions to reinforce these concepts with targeted drills
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

ActiveRecord: Fixing N+1 Query Problems in Ruby on Rails
Complete guide to detecting and fixing N+1 query problems in Rails with ActiveRecord. Master includes, preload, eager_load and automated detection tools.

Ruby on Rails Interview Questions: Top 25 in 2026
The 25 most asked Ruby on Rails interview questions. MVC architecture, Active Record, migrations, RSpec testing, REST APIs with detailed answers and code examples.

Ruby on Rails 7: Hotwire and Turbo for Reactive Applications
Complete guide to Hotwire and Turbo in Rails 7. Learn to build reactive applications without writing JavaScript using Turbo Drive, Frames, and Streams.