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:
- Eliminate the RPC server (no channel logic needed)
- Eliminate Redis or Solid Cable (in-process subscriptions)
- Eliminate the standalone cable server (in-process WebSocket handling)
- Incorporate a simple WebSocket protocol directly into your application
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:
- Single-server deployments
- Multi-tenant applications with process-per-tenant isolation
- Applications where real-time is server→client only (dashboards, progress bars, notifications)
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:
- Intercepts
/cableWebSocket upgrade requests - Uses
rack.hijackto take over the TCP connection - Implements RFC 6455 WebSocket protocol (handshake, frames, ping/pong)
- Tracks subscriptions in-memory (no Redis, no database)
- Broadcasts via HTTP POST to
/_broadcastendpoint 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):
- ✅ Turbo Stream broadcasts (DOM updates)
- ✅ Custom JSON data (progress bars, charts, widgets)
- ❌ Client→server channel actions
For client→server communication, use HTTP requests (forms, fetch, Turbo). This is exactly what most real-time features need:
- Live dashboards updating from server data
- Progress indicators for background jobs
- Status updates and notifications
- Real-time scoring and leaderboards
If you need bidirectional WebSocket communication (chat, collaborative editing, real-time drawing), stick with Action Cable.
When To Use TurboCable
Perfect for:
- ✅ Single-server applications
- ✅ Multi-tenant apps with process-per-tenant isolation
- ✅ Development environments (no Redis to install)
- ✅ Server→client real-time updates only
Not appropriate for:
- ❌ Horizontally scaled apps with multiple servers serving the same application
- ❌ Load-balanced production with shared state across instances
- ❌ Bidirectional WebSocket communication (client actions)
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:
- Multiple judges entering scores simultaneously
- Live displays updating for audiences
- Progress bars for long operations (playlist generation, PDF creation)
- Background job status updates
- Real-time heat counters during competitions
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:
- Copies a Stimulus controller to
app/javascript/controllers/turbo_streams_controller.js - 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:
ENV['TURBO_CABLE_PORT']- explicit overrideENV['PORT']- use if set3000- 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:
- TurboCable README
- EXAMPLES.md - Real-world patterns from production
- Showcase application - Full multi-tenant implementation
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.