rubyguides

ActionCable WebSockets in Rails

ActionCable WebSockets in Rails give you a native way to add real-time updates, live notifications, and fast collaborative experiences to modern web applications. Users expect instant updates, and ActionCable is Rails’ native solution for adding WebSocket functionality to your applications while keeping the API inside the Rails ecosystem.

Intro Context

ActionCable fits best when the browser needs to hear about changes right away instead of waiting for the next refresh. Chat rooms, dashboard counters, status updates, and collaborative editors all benefit from a persistent connection. Because ActionCable stays inside the Rails conventions you already know, you can combine WebSockets with authentication, models, and partial rendering without adding a separate realtime stack.

If you want the official reference while you read, the Rails guide on Action Cable is the canonical overview. This tutorial focuses on the practical pieces: how messages move, where to configure the connection, and how to keep the code maintainable once the feature grows.

TL;DR

  • Use ActionCable when your UI should update immediately after an event happens.
  • Configure the cable endpoint and adapter first, then create channels for each stream of messages.
  • Keep authorization at both the connection layer and the channel layer.
  • Use Redis in production, because the async adapter is only suitable for local development.

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. The mount directive in the routes file tells Rails to accept WebSocket upgrade requests at the /cable path and hand them off to ActionCable’s connection handler. This is the single server-side entry point for all real-time traffic:

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.

Redis matters because each Rails worker can share the same pub/sub backend. Without that shared broker, one process would not know what another process broadcast, which is why the async adapter is fine locally but not enough in production.

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.

That consumer object is the JavaScript half of the bridge. It keeps the websocket alive, handles reconnects, and gives you a place to define callbacks for connect, disconnect, and received data.

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)

The generator gives you a matching server file and client file, which keeps the subscription name and the browser-side consumer in sync. That symmetry matters, because ActionCable works best when the message name and the stream name stay easy to trace.

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.

The server-side channel is where you decide what each subscriber is allowed to hear. It is also where you connect incoming payloads to model changes, background jobs, or any other Rails code that should run after a message arrives.

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>
    `)
  }
})

The client-side channel turns incoming websocket traffic into browser behavior. You can update the DOM, render notifications, or hand the payload off to a framework like Stimulus or Turbo.

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.

Choose between raw broadcasts and rendered partials based on where you want the presentation logic to live. Raw payloads are flexible, while partials keep the HTML in the view layer and make it easier to change later.

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.

This pattern is handy when the database record itself should trigger the UI update. It keeps the websocket code close to the model event, which makes it easier to understand than scattering broadcast calls through controllers.

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

There are two auth checkpoints here. The connection decides whether the browser can open a websocket at all, while the channel decides which streams that authenticated user may join. Keeping both layers explicit makes later debugging much easier.

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. Add the Redis gem to your Gemfile and configure the cable adapter to point at your Redis instance so that broadcasts reach all server processes, not just the one that originated the message:

# Gemfile
gem "redis", "~> 4.0.1"

2. limit connection pool size

Configure your Redis connection pool. The max_pool_size setting controls how many simultaneous Redis connections ActionCable can open, and the timeout value determines how long it waits before giving up on a connection. Tune these based on your expected concurrent WebSocket count:

# 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 to monitor connection and channel counts in production. The statistics method gives you a snapshot of how many clients are connected and how many channels are active, which is useful for capacity planning and spotting unusual spikes:

ActionCable.server.statistics
# => { connections: 150, channels: 230 }

Production tuning mostly comes down to keeping connection counts predictable and avoiding expensive work on every incoming message. If you can batch updates or collapse repeated events, you reduce pressure on the websocket layer and keep the app easier to scale.

4. graceful degradation

Always have a fallback for clients that don’t support WebSockets. The JavaScript snippet below checks for ActionCable support and falls back to AJAX polling on an interval, which keeps the feature working for older browsers or network environments that block WebSocket connections:

if (window.ActionCable) {
  // Use WebSocket
} else {
  // Poll via AJAX
  setInterval(fetchMessages, 5000)
}

Advanced patterns

private channels

For user-specific streams. The stream_for method accepts a model instance and creates a named stream keyed to that record, so you can target broadcasts to a single user’s notification channel without leaking messages to other connected clients:

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

Presence tracking is usually best treated as a separate concern from the main chat payloads. That keeps the message stream simple and lets you update online state, avatars, or typing indicators independently from the content itself.

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

Tests are where you verify both halves of the connection: that a subscription opens successfully and that a broadcast reaches the right stream. If you only test the broadcast, it is easy to miss a channel authorization bug.

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/
]

Once the websocket path works, the next thing to review is how the rest of the Rails app feeds it data. Many projects pair ActionCable with background jobs, Turbo Streams, or model callbacks so that new events reach the browser without extra controller logic. That next layer is where the feature starts to feel polished, because the transport is already in place.

summary

ActionCable is the part of Rails that keeps browsers and servers talking in real time. Use it when polling is too slow, keep the channel logic narrow, and remember that authorization happens both when the connection opens and when a subscription starts. In production, Redis and careful connection management matter more than the exact shape of the JavaScript consumer.

ActionCable makes adding real-time features to Rails applications straightforward. By using WebSockets and integrating with Rails’ existing architecture, you can build interactive features that keep users updated without managing a separate realtime service.

See Also