ActionCable in Rails – Real-Time Features, Architecture & Production Guide

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 in app/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 (like ChatChannel or NotificationChannel). Channels are similar to controllers but for WebSocket streams.
  • 4. Stream Setup with Redis:
    When the server calls stream_from or stream_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:
    ActionCable.server.broadcast(\"chat_room_1\", { message: \"Hello\" })
    This data is published to Redis, and all connected clients subscribed to 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:

  1. User types “Hello” and clicks send.
  2. The message is sent to the backend via WebSocket.
  3. Rails broadcasts it using ActionCable.server.broadcast.
  4. Redis distributes the message to all servers (even if horizontally scaled).
  5. 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

1. How does ActionCable scale?

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
2. How to broadcast to a specific user?

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" })
3. Where is authentication done?

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
4. How to handle disconnects?

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
5. Can I use ActionCable with Turbo?

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>
6. How does the frontend subscribe to a channel?

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);
    }
  });
7. What data format is used in ActionCable messages?

ActionCable sends JSON messages. The backend and frontend communicate using JSON-formatted payloads.

{ message: "Hello World", user: "Alice" }
8. Can I call methods from the frontend?

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!" });
9. What happens if Redis is down?

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

10. How do I test ActionCable channels?

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

✅ Use Redis as adapter (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
✅ Authenticate users in 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
✅ Use 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!" })
✅ Broadcast only what’s needed (not large payloads)

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 }
✅ Run ActionCable on a separate process if needed

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.

✅ Monitor Redis usage and WebSocket connection counts

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
✅ Use background jobs for long or async work

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)
✅ Implement reconnect logic on frontend

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

1. Native integration with Rails

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.

2. No external service required

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.

3. Good for mid-scale real-time features

ActionCable is perfect for small to medium-sized apps with hundreds or a few thousand concurrent connections (e.g., live chat, dashboards, notifications).

4. Secure via connection authentication

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

1. Not ideal for massive scale (100K+ connections)

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.

2. Tightly coupled to Rails

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.

3. Can be memory-intensive without scaling properly

Each WebSocket connection is long-lived and consumes memory. Without Redis and proper worker separation, this can slow down your app over time.

4. No analytics or message history by default

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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top