intertwingly

It’s just data

TurboCable - Real-Time Rails Without External Dependencies


Part 1 of 3: Part 2: Shared-Nothing Architecture | Part 3: Development Process


Action Cable has been the standard way to add real-time features to Rails since 2015. It's powerful—supporting full bidirectional communication, custom channel logic, and complex real-time interactions. But that power comes with overhead: ~153MB for the cable server plus ~13MB for Redis or Solid Cable. That's ~163MB per machine just for WebSocket infrastructure.

TurboCable reduces this to ~18MB total (89% reduction, saving 145MB per machine). For applications running across multiple regions, this translates to significant infrastructure savings—the showcase application saves 1.16 GB across 8 machines.

The Action Cable documentation strongly recommends running a separate standalone cable server in production, but doesn't explain why. The reason: Action Cable's rich feature set requires significant resources, and isolating it prevents it from impacting your main Rails processes.

AnyCable is a performance-oriented alternative that moves WebSocket handling to a lightweight Go server (~20MB), with an RPC server running your channel logic (~140MB). This improves performance but adds infrastructure: you're now running two separate servers (cable + RPC), still need Redis or Solid Cable for pub/sub, and coordinating between all of them.

A Different Approach

When Turbo arrived in 2021 (successor to Turbolinks), it brought a simpler model for real-time updates: Turbo Streams. Instead of bidirectional channel logic, Turbo Streams focus on one direction: server → client DOM updates.

Here's the key insight: Rails already has excellent tools for client → server communication—HTTP GET, POST, PATCH, DELETE, plus forms and Turbo. If all you need to add is asynchronous HTML and JSON going the other direction, significant simplification is possible.

By focusing exclusively on Turbo Streams (both HTML and JSON), you can:

TurboCable implements this simplified approach: ~18MB total for in-process WebSockets with zero external dependencies.

When constraints align—when WebSocket scope matches process scope, when database scope matches tenant scope—architecture becomes simpler, not more complex. This is the core insight behind TurboCable, and it's part of a larger architectural approach to building cost-effective multi-tenant applications. See how the pieces fit together in Shared-Nothing Multi-Tenancy with SQLite, TurboCable, and Navigator.

For many applications, this is all you need:

What TurboCable Does

TurboCable provides the same Turbo Streams broadcasting API as Action Cable, but uses in-process WebSocket handling:

# Gemfile
gem 'turbo_cable'
bundle install
rails generate turbo_cable:install

Your views stay identical:

<div id="scores-board">
  <%= turbo_stream_from "live-scores" %>
  <%= render @scores %>
</div>

Your models use the same broadcast methods:

class Score < ApplicationRecord
  after_save do
    broadcast_replace_later_to "live-scores",
      partial: "scores/score",
      target: dom_id(self)
  end
end

Migration effort: If you're only using stream_from channels (no custom actions), it's zero code changes. Just delete the channel files and add turbo_stream_from in views.

How It Works

TurboCable uses Rack middleware to handle WebSocket connections:

  1. Intercepts /cable WebSocket upgrade requests
  2. Uses rack.hijack to take over the TCP connection
  3. Implements RFC 6455 WebSocket protocol (handshake, frames, ping/pong)
  4. Tracks subscriptions in-memory (no Redis, no database)
  5. Broadcasts via HTTP POST to /_broadcast endpoint in same process

When a model broadcasts an update, it renders the partial, generates Turbo Stream HTML, and POSTs to the local /_broadcast endpoint. The middleware distributes to all subscribed WebSocket clients.

No external dependencies. No Redis, no Solid Cable, no message queue. Just Ruby's standard library and Rack.

Memory Comparison

Production measurements from the showcase application in the iad region (November 2025):

Action Cable + Redis (smooth production):

navigator          1.0%    21 MB
puma (cable)       7.6%   153 MB
redis-server       0.6%    13 MB
─────────────────────────────────
Total WebSocket:   8.2%   163 MB

TurboCable (smooth-nav staging):

navigator          0.9%    18 MB
─────────────────────────────────
Total WebSocket:   0.9%    18 MB
Stack Memory Components
Action Cable 163 MB Cable server (153 MB) + Redis (13 MB)
TurboCable 18 MB In-process WebSocket handling
Savings 89% 145 MB reduction

For the showcase application running 350+ events across 8 regional machines, this translates to 1.16 GB saved across the infrastructure.

What It Doesn't Do

TurboCable is unidirectional (server→client):

For client→server communication, use HTTP requests (forms, fetch, Turbo). This is exactly what most real-time features need:

If you need bidirectional WebSocket communication (chat, collaborative editing, real-time drawing), stick with Action Cable.

When To Use TurboCable

Perfect for:

Not appropriate for:

The key limitation: TurboCable only broadcasts within a single Rails process. For traditional horizontally-scaled architectures, this is a deal-breaker.

But for single-server or process-isolated multi-tenant applications, this constraint doesn't matter. Each process handles its own WebSocket connections, and clients never need broadcasts from other processes.

Real-World Usage

TurboCable powers real-time updates in the showcase application, handling:

All running on 8 regional machines with zero Redis instances and 1.16 GB less memory usage than Action Cable.

See EXAMPLES.md for real-world patterns from production: live scoring, progress tracking, background job output, and more.

Custom JSON Broadcasting

Beyond Turbo Stream HTML, TurboCable supports custom JSON for interactive widgets:

class OfflinePlaylistJob < ApplicationJob
  def perform(event_id, request_id)
    # Broadcast progress updates
    TurboCable::Broadcastable.broadcast_json(
      "playlist_progress_#{request_id}",
      { status: 'processing', progress: 50, message: 'Processing files...' }
    )
  end
end

JavaScript handles these via CustomEvents:

// Stimulus controller
handleMessage(event) {
  const { stream, data } = event.detail
  if (stream === this.streamValue) {
    this.updateProgressBar(data.progress, data.message)
  }
}

The same WebSocket infrastructure handles both Turbo Stream HTML (automatic DOM updates) and custom JSON (programmatic handling).

Installation & Configuration

TurboCable 1.0 is now available on RubyGems:

# Add to Gemfile
gem 'turbo_cable'

# Install
bundle install
rails generate turbo_cable:install

The generator:

  1. Copies a Stimulus controller to app/javascript/controllers/turbo_streams_controller.js
  2. Adds data-controller="turbo-streams" to your <body> tag

That's it. Restart your server and TurboCable handles all /cable WebSocket connections.

Port Configuration

By default, broadcasts connect using this priority:

  1. ENV['TURBO_CABLE_PORT'] - explicit override
  2. ENV['PORT'] - use if set
  3. 3000 - default fallback

Most applications work automatically. Override only when PORT is set incorrectly:

# When using Foreman (sets PORT=5000 but Rails runs on 3000)
ENV['TURBO_CABLE_PORT'] = '3000'

# Or specify complete URL
ENV['TURBO_CABLE_BROADCAST_URL'] = 'http://localhost:3000/_broadcast'

Development with Navigator

In production multi-tenant environments, Navigator (a Go reverse proxy) handles WebSocket routing to the correct Rails process. In development, TurboCable's Ruby implementation handles everything directly.

Both implement the same protocol, so Rails code works identically in both environments. No configuration changes needed between development and production.

Try It

TurboCable 1.0 is now available on RubyGems:

# Gemfile
gem 'turbo_cable'
bundle install
rails generate turbo_cable:install

Remove Action Cable channels that only use stream_from with no custom actions. Add turbo_stream_from in views. Done.

For complete examples and patterns, see:

The Bigger Picture

TurboCable is one piece of a coherent architecture for shared-nothing multi-tenancy. Combined with SQLite and Navigator, it enables running hundreds of isolated Rails applications with minimal memory overhead.

For the full story, see Shared-Nothing Multi-Tenancy with SQLite, TurboCable, and Navigator.


Real-time Rails without external dependencies. 89% memory reduction. No Redis, no Solid Cable, no message queue. Perfect for single-server and process-isolated deployments.

This disciplined approach to documentation—from implementation details to architectural thinking—is what made building TurboCable effective. See Disciplined use of Claude for the methodology behind this development process.

github.com/rubys/turbo_cable