nteraction patterns (UI updates, form submissions, notification badges) and only becomes a constraint when pushing beyond 5,000β10,000 concurrent WebSocket streams per node. For production systems, this means deployment manifests shrink, CI/CD pipelines stabilize, and on-call rotations no longer include Redis memory alerts.
Core Solution
Implementing database-native WebSockets in Rails 8 requires three distinct phases: dependency installation, adapter configuration, and application integration. The implementation leverages existing ActionCable and Turbo Streams APIs, meaning frontend subscriptions and backend broadcasts remain unchanged.
Step 1: Dependency Installation and Schema Generation
Add the adapter to your project dependencies and generate the required database objects.
# Add to Gemfile
gem "solid_cable"
# Install dependencies
bundle install
# Generate migration and configuration stubs
bin/rails solid_cable:install
# Apply schema changes
bin/rails db:migrate
The generator creates a solid_cable_messages table with columns for channel, payload, created_at, and an indexed id for efficient polling. This table is intentionally lightweight and designed for rapid insert/delete cycles.
Step 2: Adapter Routing Configuration
Route ActionCable traffic through the database adapter by updating the environment-specific configuration file.
# config/cable.yml
development:
adapter: solid_cable
polling_interval: 0.1.seconds
message_retention: 2.days
test:
adapter: test
production:
adapter: solid_cable
polling_interval: 0.1.seconds
message_retention: 2.days
The polling_interval controls how frequently the background worker queries the messages table. The message_retention parameter defines the TTL for broadcast payloads before automatic cleanup runs.
Step 3: Application Integration
The broadcast mechanism remains identical to previous Rails versions. Below is a production-ready example using a project management domain.
View Layer (Subscription)
<!-- app/views/projects/show.html.erb -->
<%= turbo_stream_from @project, :status_updates %>
<div id="project_timeline">
<%= render partial: "projects/timeline", locals: { events: @project.status_events } %>
</div>
Model Layer (Broadcast Trigger)
# app/models/status_event.rb
class StatusEvent < ApplicationRecord
belongs_to :project
after_create_commit :broadcast_timeline_update
private
def broadcast_timeline_update
broadcast_render_to(
project,
:status_updates,
target: "project_timeline",
partial: "projects/timeline",
locals: { events: project.status_events.order(created_at: :desc) }
)
end
end
Controller Layer (State Mutation)
# app/controllers/project_status_controller.rb
class ProjectStatusController < ApplicationController
def update
@project = Project.find(params[:project_id])
@project.status_events.create!(
actor: current_user,
action: params[:action],
metadata: params[:metadata]
)
head :ok
end
end
Architecture Decisions and Rationale
Separate Database Isolation
Rails 8 recommends routing Solid Cable through a dedicated logical database (e.g., cable instead of primary). This isolates connection pools, prevents lock contention during high-frequency broadcasts, and allows independent backup strategies. The cable database stores only transient payloads, making it safe to truncate or restore without affecting business data.
Polling vs. Push Architecture
Solid Cable uses a lightweight polling loop rather than database triggers or logical decoding. Polling is chosen for compatibility across PostgreSQL, MySQL, and SQLite, and to avoid vendor-specific extensions that complicate migrations. The 0.1-second interval strikes a balance between UI responsiveness and query overhead. At this frequency, a single worker process generates approximately 600 lightweight SELECT queries per minute, which modern indexes handle efficiently.
Automatic Pruning Mechanism
The adapter runs a background cleanup job that deletes messages older than the configured message_retention. This prevents table bloat and maintains consistent query performance. The pruning query uses a simple WHERE created_at < ? condition on an indexed column, ensuring O(log n) deletion speed regardless of historical volume.
Pitfall Guide
1. Connection Pool Exhaustion
Explanation: Solid Cable workers share the application's database connection pool by default. During traffic spikes, broadcast polling can consume available connections, causing HTTP requests to queue or timeout.
Fix: Configure a dedicated connection pool for the cable database in database.yml. Set pool: 5 for the cable adapter and ensure your Puma worker count aligns with available database connections.
2. Aggressive Polling Intervals
Explanation: Setting polling_interval below 0.05.seconds generates excessive database load without perceptible UI improvements. Human interaction latency masks sub-50ms broadcast delays.
Fix: Start at 0.1.seconds. Increase to 0.2.seconds if database CPU exceeds 40% during peak WebSocket activity. Monitor query frequency via pg_stat_statements or equivalent.
3. Missing Retention Policies
Explanation: Omitting message_retention or setting it to nil causes the messages table to grow indefinitely. Large tables degrade index performance and increase backup sizes.
Fix: Always define message_retention (1β2 days is standard). Verify cleanup jobs are running by checking solid_cable_messages row counts over time.
4. Single-Database Lock Contention
Explanation: Running Solid Cable on the primary application database introduces write contention. High-frequency broadcasts can block INSERT/UPDATE operations on business tables.
Fix: Route Solid Cable to a separate logical database. Use database.yml aliases to isolate connections. Migrate existing setups using bin/rails solid_cable:install:primary_to_cable.
5. Environment Configuration Drift
Explanation: Forgetting to set the adapter in test or staging environments causes fallback to default behavior or silent failures. CI pipelines may pass locally but fail in staging due to missing adapter configuration.
Fix: Explicitly declare adapter: solid_cable in all non-test environments. Use adapter: test only in the test block. Validate configuration with bin/rails credentials:edit for production secrets.
6. Payload Size Overload
Explanation: Broadcasting large HTML fragments or unoptimized JSON over WebSockets increases bandwidth consumption and worker processing time. Turbo Streams render partials synchronously, which can block the broadcast thread.
Fix: Stream only deltas or minimal HTML snippets. Use broadcast_replace_to instead of full re-renders when possible. Compress payloads if transmitting complex state. Monitor ActionCable.server.config.logger for slow broadcast warnings.
7. Worker Process Mismanagement
Explanation: Solid Cable runs as a background process that must be managed alongside Puma. Deployment tools that only supervise the web server will leave the cable worker unmanaged, causing silent broadcast failures after restarts.
Fix: Configure your process manager (Kamal, systemd, Docker Compose, or Procfile) to launch bin/rails solid_cable:start alongside the web server. Verify both processes are healthy using health check endpoints.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Solo developer / MVP | Solid Cable | Zero infra overhead, simplified deployment, fast iteration | $0 additional |
| Early-stage startup (1kβ10k users) | Solid Cable | Scales adequately, reduces operational complexity, unified backups | $0β$50/mo (DB tier upgrade) |
| High-scale SaaS (50k+ concurrent streams) | Redis Cluster | Lower latency, horizontal pub/sub scaling, fan-out optimization | $200β$800/mo (managed Redis) |
| Real-time trading / gaming | Dedicated Message Broker (NATS/RabbitMQ) | Sub-millisecond latency, guaranteed delivery, complex routing | $500+/mo (specialized infra) |
| Internal tools / admin dashboards | Solid Cable | Low concurrency, high reliability needs, simplified maintenance | $0 additional |
Configuration Template
# config/cable.yml
development:
adapter: solid_cable
polling_interval: 0.1.seconds
message_retention: 2.days
test:
adapter: test
production:
adapter: solid_cable
polling_interval: 0.1.seconds
message_retention: 2.days
# config/database.yml (excerpt)
primary:
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
database: myapp_production
cable:
adapter: postgresql
encoding: unicode
pool: 5
database: myapp_cable_production
url: <%= ENV["CABLE_DATABASE_URL"] %>
Quick Start Guide
- Add the adapter: Run
bundle add solid_cable and execute bin/rails solid_cable:install to generate the migration and configuration stubs.
- Apply schema: Run
bin/rails db:migrate to create the solid_cable_messages table. Verify the table exists in your database client.
- Configure routing: Update
config/cable.yml to set adapter: solid_cable for development and production. Set polling_interval: 0.1.seconds and message_retention: 2.days.
- Isolate connections: Add a
cable database entry to database.yml with a dedicated connection pool. Update your deployment configuration to route cable traffic to this database.
- Deploy and verify: Start your application and cable worker. Trigger a broadcast via your model callback and confirm real-time updates appear in the browser. Monitor database query frequency to ensure polling remains within acceptable thresholds.