AnyCable for Ruby on Rails: How Does it Improve over Action Cable?
Tags: ruby, rails
Published: 2024-05-16 11:13:16

In modern web applications, real-time communication has become more than a feature: it's gradually evolved into a necessity. Users expect instant updates, live interactions, and dynamic content.

In Rails applications, Action Cable has long been the go-to solution, harnessing WebSockets to fulfill these demands. In this article, we introduce:

- The basics of WebSockets
- How Action Cable enables real-time communication via WebSockets
- Why AnyCable was created
- How to get AnyCable up and running
- The improvements AnyCable brings to the table

Let's get started!

## The WebSocket Technology

The WebSocket technology was introduced in 2011 with the publication of [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455), titled "The WebSocket Protocol". This standardized protocol facilitated the establishment of persistent, real-time communication channels between web browsers and servers, allowing for more efficient and continuous data exchange compared to traditional HTTP.

The WebSocket technology enables continuous bi-directional communication between clients and servers, a stark departure from the request-response model of HTTP (where connections terminate after each exchange). This persistence in communication proves pivotal for real-time applications like chat platforms and gaming systems.

Before the introduction of WebSocket technology, many applications often relied on a technique called polling. Polling involves the client repeatedly sending requests to the server at specified intervals to check for updates or new information. While this method allowed for some level of real-time updates, it was less efficient than WebSocket communication. Polling could lead to unnecessary server load and increased latency, as the client had to initiate a new request each time it wanted to retrieve updated data.

WebSocket technology addressed these limitations by establishing a persistent connection between the client and the server, enabling more immediate and efficient data transmission without the need for constant polling. This shift significantly improved the responsiveness and real-time capabilities of applications, providing a seamless and dynamic experience for users, particularly in scenarios demanding instant, up-to-the-moment information.

Both Action Cable and AnyCable harness the power of web sockets to furnish Rails applications with real-time updates, liberating users from the cumbersome need to refresh pages or poll for the latest information.

## Action Cable and Real-time Communication

[Action Cable](https://edgeguides.rubyonrails.org/action_cable_overview.html) enables the incorporation of real-time features into a Rails application via web sockets. This real-time communication happens between the client side (browser) and the Rails server. A few things are important to note:

- A _connection_ forms the foundation of this client-server relationship. For every WebSocket connection, Action Cable creates one connection instance.
- A _channel_ refers to a communication pathway that allows clients to subscribe to and receive updates about specific topics or categories of information, e.g., `BookChannel`, `MovieChannel`, `SportsChannel`.
- A _subscriber_ is a client subscribed to a channel to receive information.
- _Broadcasting_ is when the server publishes information that is transmitted by the relevant channel to the subscribed client/s.
- A _stream_ is the route or pathway through which broadcasts get to the subscribers. By using `stream_from`, subscriptions can be made for broadcasting.

When a connection is established between the client and the server, Action Cable creates a connection object for that connection. With this connection, the client can subscribe to one or more channels. Action Cable broadcasts updates to all subscribed clients via their respective channels.

A simple example is a browser view showing a library book list that updates when a book is added.

```shell
# create the rails application
rails new book_list
cd book_list
# create the needed controller and model
rails g controller books index
rails g model Book name
rails db:migrate
# change the development Action Cable adapter from Async (the default one) to Redis
rails turbo:install:redis
# start the redis server
redis-server
# start the rails server
rails s
```

To get a real-time update on the book model, we employ Action Cable for broadcasting the updates. We set up Action Cable to broadcast these updates to a channel called "books". Via Turbo Streams, we create the subscriber to this channel and deliver the updates.

```ruby
 # app/models/books.rb
class Book < ApplicationRecord
  broadcasts_to -> (book) { :books }
  # Broadcasts all the activities on this model -> create, update, destroy.
end
```

We can now render the list of books:

```ruby
# app/views/rooms/_book.html.erb
<div id=<%= dom_id(book) %>> <%= book.name %> </div>
```

```ruby
# app/views/books/index.html.erb
<h1>Books</h1>
<%= turbo_stream_from "books" %>
<div id="books">
  <%= render @books %>
</div>
```

As seen below, different actions trigger a broadcast, and Action Cable broadcasts to the channel specified — "books". The view is also subsequently updated to reflect these updates.

```ruby
irb> Book.create(name: "The Avatar")
  Rendered books/_book.html.erb (Duration: 0.0ms | Allocations: 11)
[ActionCable] Broadcasting to books: "<turbo-stream action=\"append\" target=\"books\"><template><div> The Avatar </div></template></turbo-stream>"
=> #<Book:0x0000000107c13358>

irb> book_1 = Book.find(1)

irb> book_1.update(name: "The Demo")
  Rendered books/_book.html.erb (Duration: 0.0ms | Allocations: 11)
[ActionCable] Broadcasting to books: "<turbo-stream action=\"replace\" target=\"book_1\"><template><div> The Demo </div></template></turbo-stream>"
=> true

irb> book_1.destroy
[ActionCable] Broadcasting to books: "<turbo-stream action=\"remove\" target=\"book_7\"></turbo-stream>"
=> #<Book:0x0000000107bd2948
```

<Banner lang="ruby" />

In Rails, integrating Action Cable (and, as a result, real-time updates into an application) is quite easy: even a beginner can get up and running in minutes. With Action Cable, we can manage connections, channels, and broadcasting to clients without worrying about the underlying WebSocket details. It also uses the same connection for multiple subscriptions, reducing the overhead of creating new connections for each new real-time update.

## Why AnyCable for Ruby on Rails?

Action Cable is built on Ruby and has significant differences from other servers written in Golang and Erlang. [Check out a detailed summary of AnyCable vs. Action Cable benchmark findings](https://evilmartians.com/chronicles/anycable-actioncable-on-steroids). Here are the key results:

- Action Cable consumes approximately 4 times more memory when handling 20 thousand idle clients without subscriptions or transmissions.
- Action Cable requires much higher CPU usage.
- Action Cable takes around 10 times longer to broadcast to 10,000 clients. Starting at 1 second for a thousand clients, the time required increases by 1 second for every additional thousand clients.

The [AnyCable WebSocket Server](https://anycable.io/) was created to combine the beauty of Action Cable with the performance benefits gained from Golang. AnyCable handles WebSockets on a different server called [AnyCable-Go](https://github.com/anycable/anycable-go), effectively reducing the burden on your primary web application.

In addition to performance advantages, AnyCable offers **resumability**. If a client becomes disconnected from the server (e.g., due to network issues leading to an offline status), upon reconnection, the client will receive all the messages missed during the disconnection. They are also not required to re-subscribe as sessions are recovered. [See a brief demo of the resumability feature](https://youtu.be/4n1SxfK0Uqg?t=916).

View detailed information on the [recent enhancements to AnyCable](https://evilmartians.com/chronicles/enter-anycable-v1-4-reliable-real-time-features-for-apps-of-any-size).

## Integrating AnyCable

Let's replace Action Cable with AnyCable in our `BookList` example.

Install the gem:

```shell
# add the gem to your gemfile
gem "anycable-rails"

# install it
bundle install
```

Next, run the setup generator.

This generator is interactive and asks questions about:

- The type of development environment in use.
- How you would like to install the `AnyCable-Go` websocket server.
- If Heroku is being used for deployment.
- If `JWT` is the intended means of authentication.
- If you want a `Procfile.dev` file.

All of this information helps to reduce the amount of manual work you need to do.

```shell
rails g anycable:setup
```

If you did say "yes" to the generation of a `Procfile.dev`, it would look something like this:

```text
# Procfile.dev

web: bin/rails s
anycable: bundle exec anycable
ws: bin/anycable-go --port=8080
```

Hence, you do not need to manually run the rest of the commands below.

Run the AnyCable RPC server:

```shell
bundle exec anycable
```

Run the WebSocket server:

```shell
anycable-go --port=8080
```

Restart the Rails server.

In the `cable.yml` file, you'll find that the adapter changes to `any_cable` if the `ACTION_CABLE_ADAPTER` environment variable is not set.

```shell
adapter: <%= ENV.fetch("ACTION_CABLE_ADAPTER", "any_cable") %>
```

In all the `$env.rb` files, you'll find the Action Cable URL set as:

```shell
config.action_cable.url = ActionCable.server.config.url = ENV.fetch("CABLE_URL", "/cable") if AnyCable::Rails.enabled?
```

In the `any_cable.yml` file, the Redis channel is set to `__anycable__`. You can confirm this in the terminal via:

```shell
redis-cli PUBSUB CHANNELS
# 1) "__anycable__"
```

Read more about the [AnyCable configuration](https://docs.anycable.io/ruby/configuration).

In the Rails server, you'll find a routing error related to the `/cable` endpoint. You'll also find that, when you filter by `websockets` in the `Network` tab of the browser, the `Request URL` for `cable` is `ws://localhost:3000/cable`, which is wrong. We should be attempting to connect to `ws://localhost:8080/cable`, the value of `config.action_cable.url`.

To fix this, the `action_cable_meta_tag` has to be added to the `application.html.erb` file.

```ruby
# application.html.erb
<head>
# ...
  <%= action_cable_meta_tag %>
# ...
</head>
```

This resets the `url` to the set value. At this point, any broadcasted stream will show up!

## Swapping Action Cable for AnyCable in Ruby

To enable resumability, AnyCable utilizes the [Action Cable Extended Protocol](https://docs.anycable.io/misc/action_cable_protocol?id=action-cable-extended-protocol), which is implemented by the AnyCable server version 1.4 and later. This protocol is inherently supported by the [AnyCable JS client](https://github.com/anycable/anycable-client/tree/master?tab=readme-ov-file).

To integrate this with Turbo Streams, it's necessary to swap out the default Action Cable client for the AnyCable client. However, when using the `@hotwired/turbo-rails` package, the Action Cable `getConsumer` function is invoked before the `setConsumer` function during the initial page load, leading to the consumer not being overridden, and two web socket connections being opened. The [@anycable/turbo-stream](https://github.com/anycable/anycable-client/blob/master/packages/turbo-stream/README.md) package was created to handle this ([read additional details about this issue](https://anycable.io/blog/anycasts-using-anycable-client/)).

Here's the procedure for this swap.

Add the `@anycable/web` and `@anycable/turbo-stream` packages:

```shell
bin/importmap pin @anycable/web @anycable/turbo-stream
```

This pins the stated packages, along with `@anycable/core`, `@hotwired/turbo`, and `nanoevents` to your `importmap.rb` file.

Create the `anycable` client:

```javascript
// app/javascript/cable.js
import { createCable } from "@anycable/web";

export default createCable();
```

Refer to this as the new "cable" client:

```ruby
# config/importmap.rb
pin "cable", to: "cable.js", preload: true
```

Start the new cable client:

```javascript
// app/javascript/application.js
// We no longer need @hotwired/turbo-rails
import "@hotwired/turbo";
import { start } from "@anycable/turbo-stream";
import cable from "cable";

start(cable);
```

With this, we can restart our server, and the consumer/client is replaced with AnyCable.

Read more about [advanced configurations for the Turbo Streams package](https://github.com/anycable/anycable-client/blob/master/packages/turbo-stream/README.md).

With the client replaced, [resumability can be implemented](https://docs.anycable.io/anycable-go/reliable_streams?id=quick-start).

With resumable sessions, the supported adapters are `http` and `redisx` (if you use Redis). It is, however, important to note that the broker `preset` command automatically sets the adapter as `http`. Hence, when using `redisx`, it must specified explicitly like so:

```shell
$ anycable-go --broker=memory --broadcast_adapter=redisx
```

There you go! You should have resumable sessions working!

## Community Adoption and Feedback

AnyCable has existed for several years and has gained widespread adoption among numerous organizations. Adopters have cited several compelling reasons for its attractiveness, including:

- Seamless migration without the need for architectural changes.
- No requirement to modify Action Cable channels or connections.
- Utilization of a dedicated server for WebSockets.
- Ability to handle a substantial number of concurrent connections without encountering any problems.

[See a few testimonials from satisfied users](https://github.com/orgs/anycable/discussions/68).

## Wrapping Up

In this post, we've seen that AnyCable offers performance enhancements to Action Cable and can be used as an alternative when dealing with a large number of connections or high traffic.

With recent improvements to AnyCable (such as the introduction of **reliable streams**), AnyCable has become a **solid choice regardless of performance concerns**. It supports broadcasting and subscription confirmation, thereby making it consistent and reliable. It can also be used in [non-Rails applications](https://docs.anycable.io/ruby/non_rails), and to [power serverless JavaScript applications](https://docs.anycable.io/guides/serverless).

Overall, [AnyCable](https://anycable.io/) is a reliable choice for modern web applications and supports a variety of servers, allowing for greater flexibility in development.

Happy coding!

**P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, [subscribe to our Ruby Magic newsletter and never miss a single post](https://blog.appsignal.com/ruby-magic)!**