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 and WebSockets in Ruby on Rails deep dive illustration

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.

app/javascript/channels/chat_channel.jsjavascript
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.

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 %>
<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.

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 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 to reinforce these concepts with targeted drills

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Tags

#ruby on rails
#action cable
#websockets
#real-time
#solid cable
#rails 8

Share

Related articles