Introduction

If you’ve ever wanted your Rails app to feel alive, you should check out Action Cable. It’s Rails’ built-in way of doing WebSockets, which basically means your server and the browser can stay connected and chat in real time. No more page refreshes, no clunky polling — updates just show up instantly.

In this guide, we’ll build a fun little demo: a live traffic counter. When someone lands on your site, we’ll log that visit in the database, bump some counters in memory, and broadcast the updated numbers to everyone online. Close the tab? The online user count drops immediately. It’s simple, but it shows the real-time magic that Action Cable brings to Rails 7.

Step 1: The Visit Model

First up, let’s make a model to log visits.

bin/rails g model Visit session_id:string ip:string user_agent:string
bin/rails db:migrate
# app/models/visit.rb
class Visit < ApplicationRecord
end

Step 2: Creating a Custom Connection

Rails already gives you two base classes in app/channels/application_cable/:
- channel.rb → the base class for all your channels.
- connection.rb → the base class for all your WebSocket connections.

We don’t normally edit those. Instead, we extend them by creating our own connection class inside app/channels/. This keeps the defaults clean and puts our logic in a clear place.

# app/channels/traffic_connection.rb
module ApplicationCable
  class TrafficConnection < Connection
    identified_by :session_id, :ip, :user_agent

    def connect
      self.session_id = request.session.id
      self.ip         = request.ip
      self.user_agent = request.user_agent
    end
  end
end

Step 3: Counters in Memory

We’ll use atomic counters to make things fast. At boot, we grab the total visits from the DB, and start online users at zero.

# config/initializers/traffic_counter.rb
$online_users = Concurrent::AtomicFixnum.new(0)
$total_visits = Concurrent::AtomicFixnum.new(Visit.count)

Step 4: The Channel

Here’s where the fun happens — log the visit, update counters, and broadcast.

# app/channels/traffic_channel.rb
class TrafficChannel < ApplicationCable::Channel
  def subscribed
    # log visit
    Visit.create!(session_id: connection.session_id,
                  ip: connection.ip,
                  user_agent: connection.user_agent)

    # update counters
    $online_users.increment
    $total_visits.increment

    stream_from "traffic_channel"
    broadcast_counts
  end

  def unsubscribed
    $online_users.decrement
    broadcast_counts
  end

  private

  def broadcast_counts
    ActionCable.server.broadcast("traffic_channel", {
      online: $online_users.value,
      total:  $total_visits.value
    })
  end
end

Step 5: The JavaScript Side

When you run bin/rails generate channel Traffic, Rails creates two files: the Ruby channel and the JavaScript subscription file. By default, the JS file imports consumer.js, which Rails also set up for you when the app was generated.

// app/javascript/channels/traffic_channel.js
import consumer from "./consumer"

consumer.subscriptions.create("TrafficChannel", {
  received(data) {
    document.querySelector("#online-count").textContent = data.online
    document.querySelector("#total-count").textContent = data.total
  }
})

The consumer.js file is generated by Rails when you create a new app. It sets up the global WebSocket connection to /cable and looks like this:

// app/javascript/channels/consumer.js
import { createConsumer } from "@rails/actioncable"

export default createConsumer()

Step 6: The View

<h1>🚦 Live Traffic</h1>
<p>Currently online: <span id="online-count">0</span></p>
<p>Total visits: <span id="total-count"><%= Visit.count %></span></p>

Step 7: Routes

# config/routes.rb
Rails.application.routes.draw do
  root "home#index"
  mount ActionCable.server => "/cable"
end

Step 8: Try It Out

Run your app with bin/dev. Open your site in a few tabs. The online users counter ticks up with each one, and drops when you close them. Total visits just keeps going up. Super satisfying to watch! 

Notes for Production

- Multiple Puma workers? Use Redis for counters — memory won’t sync across processes.
- Want to track actual users instead of sessions? Add current_user in the connection.
- Storing visits in the DB means you can run analytics later, like daily traffic or browser breakdowns.

And that’s it — a real-time live traffic counter in Rails 7 with Action Cable. In under 100 lines of code, you’ve got instant updates and a taste of what WebSockets can do. This same setup works for notifications, dashboards, and all kinds of real-time features. Once you’ve tried it, you’ll want to sprinkle it everywhere.