# 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. - Published: 2026-05-07 - Updated: 2026-05-07 - Author: SharpSkill - Tags: ruby on rails, action cable, websockets, real-time, solid cable, rails 8 - Reading time: 11 min --- 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. > **Key Takeaway for Interviews** > > 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. ```ruby # 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 end ``` The 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. ```ruby # 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 end ``` Channels 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. ```javascript // app/javascript/channels/chat_channel.js 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", `
${data.user}: ${data.body}
` ) }, 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. ## 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. ```yaml # 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.day ``` Solid 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. ```ruby # config/database.yml (Rails 8 multi-database) production: primary: <<: *default database: myapp_production cable: <<: *default database: myapp_cable_production migrations_paths: db/cable_migrate ``` Solid 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. ```ruby # 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 end ``` Model 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. ```ruby # 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 %>
<%= render @chat_room.messages %>
``` 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. ```ruby # config/cable.yml — Redis adapter for high-scale production production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: myapp_production ``` Redis 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](https://anycable.io/) 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. > **Interview Insight** > > 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. ```ruby # 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 end ``` The `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. > **Common Mistake** > > 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 channel `subscribed` callbacks - 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`, and `assert_broadcast_on` — no real WebSocket connections needed - [Practice ActionCable & WebSockets interview questions](/en/technologies/ruby-on-rails/interview-questions/action-cable-websockets) to reinforce these concepts with targeted drills --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/ruby-on-rails/action-cable-websockets-complete-guide