Action Cable Overview
In this guide, you will learn how Action Cable works and how to use WebSockets toincorporate real-time features into your Rails application.
After reading this guide, you will know:
- What Action Cable is and its integration backend and frontend
- How to set up Action Cable
- How to set up channels
- Deployment and Architecture setup for running Action Cable
1. What is Action Cable?
Action Cable seamlessly integratesWebSockets with the rest of yourRails application. It allows for real-time features to be written in Ruby in thesame style and form as the rest of your Rails application, while still beingperformant and scalable. It's a full-stack offering that provides both aclient-side JavaScript framework and a server-side Ruby framework. You haveaccess to your entire domain model written with Active Record or your ORM ofchoice.
2. Terminology
Action Cable uses WebSockets instead of the HTTP request-response protocol.Both Action Cable and WebSockets introduce some less familiar terminology:
2.1. Connections
Connections form the foundation of the client-server relationship.A single Action Cable server can handle multiple connection instances. It has oneconnection instance per WebSocket connection. A single user may have multipleWebSockets open to your application if they use multiple browser tabs or devices.
2.2. Consumers
The client of a WebSocket connection is called theconsumer. In Action Cable,the consumer is created by the client-side JavaScript framework.
2.3. Channels
Each consumer can, in turn, subscribe to multiplechannels. Each channelencapsulates a logical unit of work, similar to what a controller does ina typical MVC setup. For example, you could have aChatChannel andanAppearancesChannel, and a consumer could be subscribed to eitheror both of these channels. At the very least, a consumer should be subscribedto one channel.
2.4. Subscribers
When the consumer is subscribed to a channel, they act as asubscriber.The connection between the subscriber and the channel is, surprise-surprise,called a subscription. A consumer can act as a subscriber to a given channelany number of times. For example, a consumer could subscribe to multiple chat roomsat the same time. (And remember that a physical user may have multiple consumers,one per tab/device open to your connection).
2.5. Pub/Sub
Pub/Sub orPublish-Subscribe refers to a message queue paradigm whereby senders ofinformation (publishers), send data to an abstract class of recipients(subscribers), without specifying individual recipients. Action Cable uses thisapproach to communicate between the server and many clients.
2.6. Broadcastings
A broadcasting is a pub/sub link where anything transmitted by the broadcaster issent directly to the channel subscribers who are streaming that named broadcasting.Each channel can be streaming zero or more broadcastings.
3. Server-Side Components
3.1. Connections
For every WebSocket accepted by the server, a connection object is instantiated. Thisobject becomes the parent of all thechannel subscriptions that are createdfrom thereon. The connection itself does not deal with any specific applicationlogic beyond authentication and authorization. The client of a WebSocketconnection is called the connectionconsumer. An individual user will createone consumer-connection pair per browser tab, window, or device they have open.
Connections are instances ofApplicationCable::Connection, which extendsActionCable::Connection::Base. InApplicationCable::Connection, youauthorize the incoming connection and proceed to establish it if the user canbe identified.
3.1.1. Connection Setup
# app/channels/application_cable/connection.rbmoduleApplicationCableclassConnection<ActionCable::Connection::Baseidentified_by:current_userdefconnectself.current_user=find_verified_userendprivatedeffind_verified_userifverified_user=User.find_by(id:cookies.encrypted[:user_id])verified_userelsereject_unauthorized_connectionendendendendHereidentified_by designates a connection identifier that can be used to find thespecific connection later. Note that anything marked as an identifier will automaticallycreate a delegate by the same name on any channel instances created off the connection.
This example relies on the fact that you will already have handled authentication of the usersomewhere else in your application, and that a successful authentication sets an encryptedcookie with the user ID.
The cookie is then automatically sent to the connection instance when a new connectionis attempted, and you use that to set thecurrent_user. By identifying the connectionby this same current user, you're also ensuring that you can later retrieve all openconnections by a given user (and potentially disconnect them all if the user is deletedor unauthorized).
If your authentication approach includes using a session, you use cookie store for thesession, your session cookie is named_session and the user ID key isuser_id youcan use this approach:
verified_user=User.find_by(id:cookies.encrypted["_session"]["user_id"])3.1.2. Exception Handling
By default, unhandled exceptions are caught and logged to Rails' logger. If you would like toglobally intercept these exceptions and report them to an external bug tracking service, forexample, you can do so withrescue_from:
# app/channels/application_cable/connection.rbmoduleApplicationCableclassConnection<ActionCable::Connection::Baserescue_fromStandardError,with: :report_errorprivatedefreport_error(e)SomeExternalBugtrackingService.notify(e)endendend3.1.3. Connection Callbacks
ActionCable::Connection::Callbacks provides callback hooks that areinvoked when sending commands to the client, such as when subscribing,unsubscribing, or performing an action:
3.2. Channels
Achannel encapsulates a logical unit of work, similar to what a controller does in atypical MVC setup. By default, Rails creates a parentApplicationCable::Channel class(which extendsActionCable::Channel::Base) for encapsulating shared logic between your channels,when you use the channel generator for the first time.
3.2.1. Parent Channel Setup
# app/channels/application_cable/channel.rbmoduleApplicationCableclassChannel<ActionCable::Channel::BaseendendYour own channel classes could then look like these examples:
# app/channels/chat_channel.rbclassChatChannel<ApplicationCable::Channelend# app/channels/appearance_channel.rbclassAppearanceChannel<ApplicationCable::ChannelendA consumer could then be subscribed to either or both of these channels.
3.2.2. Subscriptions
Consumers subscribe to channels, acting assubscribers. Their connection iscalled asubscription. Produced messages are then routed to these channelsubscriptions based on an identifier sent by the channel consumer.
# app/channels/chat_channel.rbclassChatChannel<ApplicationCable::Channel# Called when the consumer has successfully# become a subscriber to this channel.defsubscribedendend3.2.3. Exception Handling
As withApplicationCable::Connection, you can also userescue_from on aspecific channel to handle raised exceptions:
# app/channels/chat_channel.rbclassChatChannel<ApplicationCable::Channelrescue_from"MyError",with: :deliver_error_messageprivatedefdeliver_error_message(e)# broadcast_to(...)endend3.2.4. Channel Callbacks
ActionCable::Channel::Callbacks provides callback hooks that are invokedduring the life cycle of a channel:
before_subscribeafter_subscribe(aliased ason_subscribe)before_unsubscribeafter_unsubscribe(aliased ason_unsubscribe)
4. Client-Side Components
4.1. Connections
Consumers require an instance of the connection on their side. This can beestablished using the following JavaScript, which is generated by default by Rails:
4.1.1. Connect Consumer
// app/javascript/channels/consumer.js// Action Cable provides the framework to deal with WebSockets in Rails.// You can generate new channels where WebSocket features live using the `bin/rails generate channel` command.import{createConsumer}from"@rails/actioncable"exportdefaultcreateConsumer()This will ready a consumer that'll connect against/cable on your server by default.The connection won't be established until you've also specified at least one subscriptionyou're interested in having.
The consumer can optionally take an argument that specifies the URL to connect to. Thiscan be a string or a function that returns a string that will be called when theWebSocket is opened.
// Specify a different URL to connect tocreateConsumer('wss://example.com/cable')// Or when using websockets over HTTPcreateConsumer('https://ws.example.com/cable')// Use a function to dynamically generate the URLcreateConsumer(getWebSocketURL)functiongetWebSocketURL(){consttoken=localStorage.get('auth-token')return`wss://example.com/cable?token=${token}`}4.1.2. Subscriber
A consumer becomes a subscriber by creating a subscription to a given channel:
// app/javascript/channels/chat_channel.jsimportconsumerfrom"./consumer"consumer.subscriptions.create({channel:"ChatChannel",room:"Best Room"})// app/javascript/channels/appearance_channel.jsimportconsumerfrom"./consumer"consumer.subscriptions.create({channel:"AppearanceChannel"})While this creates the subscription, the functionality needed to respond toreceived data will be described later on.
A consumer can act as a subscriber to a given channel any number of times. Forexample, a consumer could subscribe to multiple chat rooms at the same time:
// app/javascript/channels/chat_channel.jsimportconsumerfrom"./consumer"consumer.subscriptions.create({channel:"ChatChannel",room:"1st Room"})consumer.subscriptions.create({channel:"ChatChannel",room:"2nd Room"})5. Client-Server Interactions
5.1. Streams
Streams provide the mechanism by which channels route published content(broadcasts) to their subscribers. For example, the following code usesstream_from to subscribe to the broadcasting namedchat_Best Room whenthe value of the:room parameter is"Best Room":
# app/channels/chat_channel.rbclassChatChannel<ApplicationCable::Channeldefsubscribedstream_from"chat_#{params[:room]}"endendThen, elsewhere in your Rails application, you can broadcast to such a room bycallingbroadcast:
ActionCable.server.broadcast("chat_Best Room",{body:"This Room is Best Room."})If you have a stream that is related to a model, then the broadcasting namecan be generated from the channel and model. For example, the following codeusesstream_for to subscribe to a broadcasting likeposts:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE, whereZ2lkOi8vVGVzdEFwcC9Qb3N0LzE isthe GlobalID of the Post model.
classPostsChannel<ApplicationCable::Channeldefsubscribedpost=Post.find(params[:id])stream_forpostendendYou can then broadcast to this channel by callingbroadcast_to:
PostsChannel.broadcast_to(@post,@comment)5.2. Broadcastings
Abroadcasting is a pub/sub link where anything transmitted by a publisheris routed directly to the channel subscribers who are streaming that namedbroadcasting. Each channel can be streaming zero or more broadcastings.
Broadcastings are purely an online queue and time-dependent. If a consumer isnot streaming (subscribed to a given channel), they'll not get the broadcastshould they connect later.
5.3. Subscriptions
When a consumer is subscribed to a channel, they act as a subscriber. Thisconnection is called a subscription. Incoming messages are then routed tothese channel subscriptions based on an identifier sent by the cable consumer.
// app/javascript/channels/chat_channel.jsimportconsumerfrom"./consumer"consumer.subscriptions.create({channel:"ChatChannel",room:"Best Room"},{received(data){this.appendLine(data)},appendLine(data){consthtml=this.createLine(data)constelement=document.querySelector("[data-chat-room='Best Room']")element.insertAdjacentHTML("beforeend",html)},createLine(data){return` <article> <span>${data["sent_by"]}</span> <span>${data["body"]}</span> </article> `}})5.4. Passing Parameters to Channels
You can pass parameters from the client-side to the server-side when creating asubscription. For example:
# app/channels/chat_channel.rbclassChatChannel<ApplicationCable::Channeldefsubscribedstream_from"chat_#{params[:room]}"endendAn object passed as the first argument tosubscriptions.create becomes theparams hash in the cable channel. The keywordchannel is required:
// app/javascript/channels/chat_channel.jsimportconsumerfrom"./consumer"consumer.subscriptions.create({channel:"ChatChannel",room:"Best Room"},{received(data){this.appendLine(data)},appendLine(data){consthtml=this.createLine(data)constelement=document.querySelector("[data-chat-room='Best Room']")element.insertAdjacentHTML("beforeend",html)},createLine(data){return` <article> <span>${data["sent_by"]}</span> <span>${data["body"]}</span> </article> `}})# Somewhere in your app this is called, perhaps# from a NewCommentJob.ActionCable.server.broadcast("chat_#{room}",{sent_by:"Paul",body:"This is a cool chat app."})5.5. Rebroadcasting a Message
A common use case is torebroadcast a message sent by one client to anyother connected clients.
# app/channels/chat_channel.rbclassChatChannel<ApplicationCable::Channeldefsubscribedstream_from"chat_#{params[:room]}"enddefreceive(data)ActionCable.server.broadcast("chat_#{params[:room]}",data)endend// app/javascript/channels/chat_channel.jsimportconsumerfrom"./consumer"constchatChannel=consumer.subscriptions.create({channel:"ChatChannel",room:"Best Room"},{received(data){// data => { sent_by: "Paul", body: "This is a cool chat app." }}})chatChannel.send({sent_by:"Paul",body:"This is a cool chat app."})The rebroadcast will be received by all connected clients,including theclient that sent the message. Note that params are the same as they were whenyou subscribed to the channel.
6. Full-Stack Examples
The following setup steps are common to both examples:
6.1. Example 1: User Appearances
Here's a simple example of a channel that tracks whether a user is online or notand what page they're on. (This is useful for creating presence features like showinga green dot next to a username if they're online).
Create the server-side appearance channel:
# app/channels/appearance_channel.rbclassAppearanceChannel<ApplicationCable::Channeldefsubscribedcurrent_user.appearenddefunsubscribedcurrent_user.disappearenddefappear(data)current_user.appear(on:data["appearing_on"])enddefawaycurrent_user.awayendendWhen a subscription is initiated thesubscribed callback gets fired, and wetake that opportunity to say "the current user has indeed appeared". Thatappear/disappear API could be backed by Redis, a database, or whatever else.
Create the client-side appearance channel subscription:
// app/javascript/channels/appearance_channel.jsimportconsumerfrom"./consumer"consumer.subscriptions.create("AppearanceChannel",{// Called once when the subscription is created.initialized(){this.update=this.update.bind(this)},// Called when the subscription is ready for use on the server.connected(){this.install()this.update()},// Called when the WebSocket connection is closed.disconnected(){this.uninstall()},// Called when the subscription is rejected by the server.rejected(){this.uninstall()},update(){this.documentIsActive?this.appear():this.away()},appear(){// Calls `AppearanceChannel#appear(data)` on the server.this.perform("appear",{appearing_on:this.appearingOn})},away(){// Calls `AppearanceChannel#away` on the server.this.perform("away")},install(){window.addEventListener("focus",this.update)window.addEventListener("blur",this.update)document.addEventListener("turbo:load",this.update)document.addEventListener("visibilitychange",this.update)},uninstall(){window.removeEventListener("focus",this.update)window.removeEventListener("blur",this.update)document.removeEventListener("turbo:load",this.update)document.removeEventListener("visibilitychange",this.update)},getdocumentIsActive(){returndocument.visibilityState==="visible"&&document.hasFocus()},getappearingOn(){constelement=document.querySelector("[data-appearing-on]")returnelement?element.getAttribute("data-appearing-on"):null}})6.1.1. Client-Server Interaction
Client connects to theServer via
createConsumer(). (consumer.js). TheServer identifies this connection bycurrent_user.Client subscribes to the appearance channel via
consumer.subscriptions.create({ channel: "AppearanceChannel" }). (appearance_channel.js)Server recognizes a new subscription has been initiated for theappearance channel and runs its
subscribedcallback, calling theappearmethod oncurrent_user. (appearance_channel.rb)Client recognizes that a subscription has been established and calls
connected(appearance_channel.js), which in turn callsinstallandappear.appearcallsAppearanceChannel#appear(data)on the server, and supplies adata hash of{ appearing_on: this.appearingOn }. This ispossible because the server-side channel instance automatically exposes allpublic methods declared on the class (minus the callbacks), so that these can bereached as remote procedure calls via a subscription'sperformmethod.Server receives the request for the
appearaction on the appearancechannel for the connection identified bycurrent_user(appearance_channel.rb).Server retrieves the data with the:appearing_onkey from the data hash and sets it as the value for the:onkey being passed tocurrent_user.appear.
6.2. Example 2: Receiving New Web Notifications
The appearance example was all about exposing server functionality toclient-side invocation over the WebSocket connection. But the great thingabout WebSockets is that it's a two-way street. So, now, let's show an examplewhere the server invokes an action on the client.
This is a web notification channel that allows you to trigger client-sideweb notifications when you broadcast to the relevant streams:
Create the server-side web notifications channel:
# app/channels/web_notifications_channel.rbclassWebNotificationsChannel<ApplicationCable::Channeldefsubscribedstream_forcurrent_userendendCreate the client-side web notifications channel subscription:
// app/javascript/channels/web_notifications_channel.js// Client-side which assumes you've already requested// the right to send web notifications.importconsumerfrom"./consumer"consumer.subscriptions.create("WebNotificationsChannel",{received(data){newNotification(data["title"],{body:data["body"]})}})Broadcast content to a web notification channel instance from elsewhere in yourapplication:
# Somewhere in your app this is called, perhaps from a NewCommentJobWebNotificationsChannel.broadcast_to(current_user,title:"New things!",body:"All the news fit to print")TheWebNotificationsChannel.broadcast_to call places a message in the currentsubscription adapter's pubsub queue under a separate broadcasting name for eachuser. For a user with an ID of 1, the broadcasting name would beweb_notifications:1.
The channel has been instructed to stream everything that arrives atweb_notifications:1 directly to the client by invoking thereceivedcallback. The data passed as an argument is the hash sent as the second parameterto the server-side broadcast call, JSON encoded for the trip across the wireand unpacked for the data argument arriving asreceived.
6.3. More Complete Examples
See therails/actioncable-examplesrepository for a full example of how to set up Action Cable in a Rails app and adding channels.
7. Configuration
Action Cable has two required configurations: a subscription adapter and allowed request origins.
7.1. Subscription Adapter
By default, Action Cable looks for a configuration file inconfig/cable.yml.The file must specify an adapter for each Rails environment. See theDependencies section for additional information on adapters.
development:adapter:asynctest:adapter:testproduction:adapter:redisurl:redis://10.10.3.153:6381channel_prefix:appname_production7.1.1. Adapter Configuration
Below is a list of the subscription adapters available for end-users.
7.1.1.1. Async Adapter
The async adapter is intended for development/testing and should not be used in production.
The async adapter only works within the same process, so for manually triggering cable updates from a console and seeing results in the browser, you must do so from the web console (running inside the dev process), not a terminal started viabin/rails console! Addconsole to any action or any ERB template view to make the web console appear.
7.1.1.2. Solid Cable Adapter
The Solid Cable adapter is a database-backed solution that uses Active Record. It has been tested with MySQL, SQLite, and PostgreSQL. Runningbin/rails solid_cable:install will automatically set upconfig/cable.yml and createdb/cable_schema.rb. After that, you must manually updateconfig/database.yml, adjusting it based on your database. SeeSolid Cable Installation.
7.1.1.3. Redis Adapter
The Redis adapter requires users to provide a URL pointing to the Redis server.Additionally, achannel_prefix may be provided to avoid channel name collisionswhen using the same Redis server for multiple applications. See theRedis Pub/Sub documentation for more details.
The Redis adapter also supports SSL/TLS connections. The required SSL/TLS parameters can be passed inssl_params key in the configuration YAML file.
production:adapter:redisurl:rediss://10.10.3.153:tls_portchannel_prefix:appname_productionssl_params:ca_file:"/path/to/ca.crt"The options given tossl_params are passed directly to theOpenSSL::SSL::SSLContext#set_params method and can be any valid attribute of the SSL context.Please refer to theOpenSSL::SSL::SSLContext documentation for other available attributes.
If you are using self-signed certificates for redis adapter behind a firewall and opt to skip certificate check, then the sslverify_mode should be set asOpenSSL::SSL::VERIFY_NONE.
It is not recommended to useVERIFY_NONE in production unless you absolutely understand the security implications. In order to set this option for the Redis adapter, the config should bessl_params: { verify_mode: <%= OpenSSL::SSL::VERIFY_NONE %> }.
7.1.1.4. PostgreSQL Adapter
The PostgreSQL adapter uses Active Record's connection pool, and thus theapplication'sconfig/database.yml database configuration, for its connection.This may change in the future.#27214
PostgreSQL has a8000 bytes limit onNOTIFY (the command used under the hood for sending notifications) which might be a constraint when dealing with large payloads.
7.2. Allowed Request Origins
Action Cable will only accept requests from specified origins, which arepassed to the server config as an array. The origins can be instances ofstrings or regular expressions, against which a check for the match will be performed.
config.action_cable.allowed_request_origins=["https://rubyonrails.com",%r{http://ruby.*}]To disable and allow requests from any origin:
config.action_cable.disable_request_forgery_protection=trueBy default, Action Cable allows all requests from localhost:3000 when runningin the development environment.
7.3. Consumer Configuration
To configure the URL, add a call toaction_cable_meta_tag in your HTML layoutHEAD. This uses a URL or path typically set viaconfig.action_cable.url in theenvironment configuration files.
7.4. Worker Pool Configuration
The worker pool is used to run connection callbacks and channel actions inisolation from the server's main thread. Action Cable allows the applicationto configure the number of simultaneously processed threads in the worker pool.
config.action_cable.worker_pool_size=4Also, note that your server must provide at least the same number of databaseconnections as you have workers. The default worker pool size is set to 4, sothat means you have to make at least 4 database connections available.You can change that inconfig/database.yml through thepool attribute.
7.5. Client-side Logging
Client-side logging is disabled by default. You can enable this by setting theActionCable.logger.enabled to true.
import*asActionCablefrom'@rails/actioncable'ActionCable.logger.enabled=true7.6. Other Configurations
The other common option to configure is the log tags applied to theper-connection logger. Here's an example that usesthe user account id if available, else "no-account" while tagging:
config.action_cable.log_tags=[->request{request.env["user_account_id"]||"no-account"},:action_cable,->request{request.uuid}]For a full list of all configuration options, see theActionCable::Server::Configuration class.
8. Running Standalone Cable Servers
Action Cable can either run as part of your Rails application, or asa standalone server. In development, running as part of your Rails appis generally fine, but in production you should run it as a standalone.
8.1. In App
Action Cable can run alongside your Rails application. For example, tolisten for WebSocket requests on/websocket, specify that path toconfig.action_cable.mount_path:
# config/application.rbclassApplication<Rails::Applicationconfig.action_cable.mount_path="/websocket"endYou can useActionCable.createConsumer() to connect to the cableserver ifaction_cable_meta_tag is invoked in the layout. Otherwise, a path isspecified as first argument tocreateConsumer (e.g.ActionCable.createConsumer("/websocket")).
For every instance of your server you create, and for every worker your serverspawns, you will also have a new instance of Action Cable, but the Redis orPostgreSQL adapter keeps messages synced across connections.
8.2. Standalone
The cable servers can be separated from your normal application server. It'sstill a Rack application, but it is its own Rack application. The recommendedbasic setup is as follows:
# cable/config.rurequire_relative"../config/environment"Rails.application.eager_load!runActionCable.serverThen to start the server:
$bundle execpuma-p 28080 cable/config.ruThis starts a cable server on port 28080. To tell Rails to use thisserver, update your config:
# config/environments/development.rbRails.application.configuredoconfig.action_cable.mount_path=nilconfig.action_cable.url="ws://localhost:28080"# use wss:// in productionendFinally, ensure you haveconfigured the consumer correctly.
8.3. Notes
The WebSocket server doesn't have access to the session, but it hasaccess to the cookies. This can be used when you need to handleauthentication. You can see one way of doing that with Devise in thisarticle.
9. Dependencies
Action Cable provides a subscription adapter interface to process itspubsub internals. By default, asynchronous, inline, PostgreSQL, and Redisadapters are included. The default adapterin new Rails applications is the asynchronous (async) adapter.
The Ruby side of things is built on top ofwebsocket-driver,nio4r, andconcurrent-ruby.
10. Deployment
Action Cable is powered by a combination of WebSockets and threads. Both theframework plumbing and user-specified channel work are handled internally byutilizing Ruby's native thread support. This means you can use all your existingRails models with no problem, as long as you haven't committed any thread-safety sins.
The Action Cable server implements the Rack socket hijacking API,thereby allowing the use of a multi-threaded pattern for managing connectionsinternally, irrespective of whether the application server is multi-threaded or not.
Accordingly, Action Cable works with popular servers like Unicorn, Puma, andPassenger.
11. Testing
You can find detailed instructions on how to test your Action Cable functionality in thetesting guide.