WebSockets in Rails 5 with ActionCable
Rails 5 brings ActionCable — WebSockets integrated into the framework. A deep-dive on channels, subscriptions, and broadcasting, how the Redis-backed pub/sub works, and the scaling realities of holding thousands of open connections.
The web was built on a request-response model: the browser asks, the server answers, the connection closes. That is a poor fit for anything live — a chat, a notification, a price ticker, a collaborative editor — where the server needs to push data to the browser without being asked. For years we faked it with polling (ask every few seconds) or long-polling (hold a request open), both awkward and wasteful. Rails 5, currently in beta, brings a proper answer into the framework: ActionCable, WebSockets integrated with the rest of Rails. Here is how it works and what to watch for.
WebSockets: a persistent, two-way pipe
A WebSocket is a single, long-lived TCP connection that stays open and lets data flow both ways at any time. After an initial HTTP handshake that “upgrades” the connection, the server can push a message to the browser the instant something happens, and the browser can send one back, all over the same connection without the overhead of new HTTP requests. Compared to polling, it is dramatically more efficient (no constant re-requesting) and genuinely real-time (no polling-interval lag). ActionCable is Rails’ framework for using WebSockets without hand-rolling the whole machinery.
The pieces: connections, channels, subscriptions
ActionCable has a small vocabulary worth getting straight:
- A Connection is established once per browser tab (per WebSocket). It handles authentication — who is on the other end — and exists for the life of the socket.
- A Channel is like a controller for WebSocket work: a logical unit such as
ChatChannelorNotificationsChannel, with actions clients can call and streams it can broadcast to. - A Subscription is a client subscribing to a channel, optionally scoped — “this user, subscribed to room 42”.
Authentication happens in the Connection, using the same session cookie as the rest of your app:
# 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
User.find_by(id: cookies.encrypted[:user_id]) || reject_unauthorized_connection
end
end
end
This is the part people skip and regret: a WebSocket is a persistent, authenticated channel into your app, and you must verify identity at connect time, because everything the connection does afterwards runs as that user.
A channel and streaming
A channel subscribes a client to a stream — a named broadcast target. When something is broadcast to that stream, every subscriber receives it:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "room_#{params[:room_id]}"
end
def speak(data)
Message.create!(room_id: params[:room_id],
user: current_user, body: data["body"])
end
end
On the server, you broadcast to that stream from anywhere — typically a model callback or, better, a background job — and every subscribed browser gets the payload pushed to it:
ActionCable.server.broadcast("room_#{message.room_id}",
user: message.user.name, body: message.body)
The client subscribes and reacts to incoming data with a little JavaScript:
App.cable.subscriptions.create({ channel: "ChatChannel", room_id: 42 }, {
received(data) {
appendMessage(data.user, data.body); // server pushed this to us
},
speak(body) {
this.perform("speak", { body }); // call the channel action
}
});
That is a working real-time chat in a few dozen lines, with the same models, authentication, and conventions as the rest of your Rails app — which is exactly the integration ActionCable is selling.
How the broadcasting actually works: Redis pub/sub
It is worth understanding the plumbing, because it explains the scaling story. When you
broadcast to a stream, ActionCable does not somehow reach into every connection
itself. It publishes the message to a pub/sub backend — Redis, in production. Every
ActionCable server process subscribes to Redis, receives the published message, and
forwards it down the WebSocket connections it is holding for subscribers of that stream.
Redis as the message bus is what lets you run multiple ActionCable processes (or servers): a message broadcast on one process reaches subscribers connected to another, because they all share Redis. Without that, a broadcast would only reach the users who happened to be connected to the same process — useless beyond one box.
The scaling realities
This is where WebSockets demand a different mindset from request-response, and where teams get surprised:
- Connections are held open, and they cost. A normal web request occupies a worker for milliseconds. A WebSocket occupies resources for as long as the user has the tab open — minutes or hours. Thousands of concurrent users means thousands of open connections held simultaneously, which is a fundamentally different load profile from serving pages.
- The cable server often wants to be separate. Because long-lived connections tie up server resources differently from short requests, it is common to run ActionCable as its own process/server, scaled independently from your regular web workers, so a surge of socket connections does not starve normal page serving.
- You need an evented server. ActionCable holds many connections per process, which requires a concurrency model suited to it (Puma in threaded/clustered mode); a process-per-request server is the wrong tool.
- Broadcast from background jobs, not request threads. Pushing a broadcast inline in a controller couples your request latency to the fan-out; do the work and broadcast from a job.
Verdict
ActionCable is a genuinely valuable addition to Rails 5: it brings real-time, server-push features into the framework with the same authentication, models, and conventions you already use, and “build a chat” goes from a multi-week integration to a few channels. The Redis-backed pub/sub design is sound and lets it scale across processes. But WebSockets are not free HTTP — holding thousands of persistent connections is a different load profile that wants an evented server, often a separate cable tier, and broadcasting from background jobs. Use ActionCable where real-time genuinely improves the product — live updates, notifications, collaboration — and go in understanding that the cost is connections held open, not requests served. Get that right and Rails can now do real-time as comfortably as it does CRUD.