ActionCable brought WebSocket support to Rails, but it traditionally required Redis as a message broker. This added complexity and cost to deployments.
Rails 8 introduces SolidCable as the default ActionCable adapter. It stores messages in the database and uses polling to deliver them to subscribers. This eliminates Redis as a dependency for real time features.
In this post, we will explore how SolidCable works, how to configure it for production, and important considerations for the application.
How SolidCable Works
Traditional ActionCable with Redis uses pub/sub: when a message is broadcast, Redis immediately pushes it to all subscribers.
SolidCable takes a different approach. Messages are written to a database table and subscribers poll for new messages. Despite the polling mechanism, performance is comparable to Redis for most use cases.
This database-driven approach was not practical in the past. Modern infrastructure with NVMe SSDs has made it viable due to extremely fast read/write speeds and low storage latency.
For PostgreSQL, SolidCable can use LISTEN/NOTIFY
for more efficient message delivery.
Installation
SolidCable is preconfigured in new Rails 8 applications. For existing applications:
bundle add solid_cable
bin/rails solid_cable:installThis creates:
- Updated
config/cable.ymlconfiguration db/cable_schema.rbfor the database schema
Database Configuration
Configure a dedicated database for SolidCable:
# config/database.yml
production:
primary:
<<: *default
database: storage/production.sqlite3
cable:
<<: *default
database: storage/production_cable.sqlite3
migrations_paths: db/cable_migrateFor PostgreSQL:
# config/database.yml
production:
primary:
<<: *default
database: myapp_production
cable:
<<: *default
database: myapp_cable_production
migrations_paths: db/cable_migrateRun the setup:
bin/rails db:prepareConfiguration Options
Configure SolidCable in config/cable.yml:
# config/cable.yml
production:
adapter: solid_cable
connects_to:
database:
writing: cable
polling_interval: 0.1.seconds
message_retention: 1.dayKey options:
polling_interval: How often clients check for new messages (default: 0.1 seconds)message_retention: How long messages are kept before cleanup (default: 1 day)autotrim: Whether to automatically clean old messages (default: true)
Real World Usage Examples
Live Notifications
Build a simple notification system that updates in real time:
# app/channels/notifications_channel.rb
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from "notifications_#{current_user.id}"
end
endBroadcast a notification from anywhere in the application:
# Broadcast from a controller, model, or job
ActionCable.server.broadcast(
"notifications_#{user.id}",
{ message: "You have a new message!" }
)Handle the notification on the client:
// app/javascript/channels/notifications_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("NotificationsChannel", {
received(data) {
alert(data.message)
}
})Live Dashboard Updates
Create a dashboard that updates metrics in real time:
# app/channels/dashboard_channel.rb
class DashboardChannel < ApplicationCable::Channel
def subscribed
stream_from "dashboard_#{params[:team_id]}"
end
endBroadcast updated metrics:
ActionCable.server.broadcast(
"dashboard_#{team.id}",
{ active_users: 42, orders_today: 156 }
)// app/javascript/channels/dashboard_channel.js
import consumer from "./consumer"
consumer.subscriptions.create(
{ channel: "DashboardChannel", team_id: 1 },
{
received(data) {
document.getElementById("active-users").textContent = data.active_users
document.getElementById("orders-today").textContent = data.orders_today
}
}
)Collaborative Editing
Sync a shared document in real time across all connected users. When one user types, everyone else sees the update immediately:
# app/channels/document_channel.rb
class DocumentChannel < ApplicationCable::Channel
def subscribed
stream_from "document_#{params[:id]}"
end
def update_content(data)
ActionCable.server.broadcast(
"document_#{params[:id]}",
{ user: current_user.name, content: data["content"] }
)
end
end// app/javascript/channels/document_channel.js
import consumer from "./consumer"
const channel = consumer.subscriptions.create(
{ channel: "DocumentChannel", id: 1 },
{
received(data) {
// Update the shared textarea for all other collaborators
document.getElementById("document-content").value = data.content
}
}
)
// Broadcast changes whenever the user types
document.getElementById("document-content").addEventListener("input", (e) => {
channel.perform("update_content", { content: e.target.value })
})Chat Application
Build a real time chat feature:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_room_#{params[:room_id]}"
end
def speak(data)
ActionCable.server.broadcast(
"chat_room_#{params[:room_id]}",
{ user: current_user.name, message: data["message"] }
)
end
end// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
const chat = consumer.subscriptions.create(
{ channel: "ChatChannel", room_id: 1 },
{
received(data) {
const messages = document.getElementById("messages")
messages.innerHTML += `<p><strong>${data.user}:</strong> ${data.message}</p>`
}
}
)
// Send a message
document.getElementById("send").addEventListener("click", () => {
const input = document.getElementById("message-input")
chat.perform("speak", { message: input.value })
input.value = ""
})Turbo Streams Integration
SolidCable works seamlessly with Turbo Streams:
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post
belongs_to :user
after_create_commit -> { broadcast_append_to post }
after_update_commit -> { broadcast_replace_to post }
after_destroy_commit -> { broadcast_remove_to post }
end<%# app/views/posts/show.html.erb %>
<%= turbo_stream_from @post %>
<div id="comments">
<%= render @post.comments %>
</div>
<%= form_with model: [@post, Comment.new] do |f| %>
<%= f.text_area :content %>
<%= f.submit "Add Comment" %>
<% end %>Message Trimming
SolidCable automatically trims old messages. Configure retention and trimming behavior:
# config/cable.yml
production:
adapter: solid_cable
message_retention: 1.hour
autotrim: trueFor high volume applications, disable autotrim and run cleanup in a background job:
# config/cable.yml
production:
adapter: solid_cable
message_retention: 1.hour
autotrim: false# config/recurring.yml
production:
trim_cable_messages:
class: SolidCable::TrimJob
schedule: "*/5 * * * *"Single Database Configuration
For simpler deployments, use a single database:
- Copy the schema into a migration:
# db/migrate/20251226000000_create_solid_cable_tables.rb
class CreateSolidCableTables < ActiveRecord::Migration[8.0]
def change
create_table :solid_cable_messages do |t|
t.binary :channel, null: false, limit: 1024
t.binary :payload, null: false, limit: 536870912
t.datetime :created_at, null: false
t.index [:channel, :created_at]
t.index :created_at
end
end
end- Update cable configuration:
# config/cable.yml
production:
adapter: solid_cable
polling_interval: 0.1.seconds
message_retention: 1.day- Run the migration:
bin/rails db:migrateCaveats and Considerations
Latency
SolidCable uses polling, which introduces latency. With the default 0.1 second polling interval, messages can take up to 100 milliseconds to arrive.
For most applications, this is acceptable. For latency sensitive features like gaming or live trading, consider Redis or AnyCable.
Reduce polling interval for lower latency:
# config/cable.yml
production:
adapter: solid_cable
polling_interval: 0.01.secondsLower intervals increase database load. Test thoroughly before deploying.
Database Load
Every connected client polls the database. With many concurrent connections, this creates significant read load.
Monitor the database and scale accordingly. Consider:
- Using a dedicated cable database
- Increasing connection pool size
- Using read replicas for polling queries
Connection Limits
Each WebSocket connection uses a database connection for polling. Ensure the database can handle the connection count:
# config/database.yml
production:
cable:
pool: <%= ENV.fetch("CABLE_DB_POOL", 50) %>Message Size Limits
SolidCable stores messages in the database. Very large messages can impact performance. Keep broadcast payloads small:
# Avoid this
ActionCable.server.broadcast("channel", { data: huge_object.to_json })
# Do this instead
ActionCable.server.broadcast("channel", { id: record.id, type: "update" })Let clients fetch full data via HTTP if needed.
No Presence Tracking
Unlike some Redis based solutions, SolidCable does not provide built in presence tracking. Implement it manually if needed:
# app/channels/presence_channel.rb
class PresenceChannel < ApplicationCable::Channel
def subscribed
@room_id = params[:room_id]
stream_from "presence_#{@room_id}"
PresenceTracker.join(@room_id, current_user)
broadcast_presence
end
def unsubscribed
PresenceTracker.leave(@room_id, current_user)
broadcast_presence
end
private
def broadcast_presence
users = PresenceTracker.users_in(@room_id)
ActionCable.server.broadcast(
"presence_#{@room_id}",
{ users: users.map { |u| { id: u.id, name: u.name } } }
)
end
end# app/services/presence_tracker.rb
class PresenceTracker
def self.join(room_id, user)
Rails.cache.write(
"presence:#{room_id}:#{user.id}",
true,
expires_in: 5.minutes
)
end
def self.leave(room_id, user)
Rails.cache.delete("presence:#{room_id}:#{user.id}")
end
def self.users_in(room_id)
# Implementation depends on the caching strategy
end
endHorizontal Scaling
SolidCable works across multiple servers because all servers read from the same database. However, ensure the database can handle the combined load.
PostgreSQL LISTEN/NOTIFY
When using PostgreSQL, SolidCable can use LISTEN/NOTIFY
for more efficient message delivery.
This reduces polling overhead
but requires PostgreSQL specific configuration.
Performance Benchmarks
These benchmarks compare the SolidCable adapter against the Redis adapter within ActionCable — they measure message delivery latency at different concurrent connection counts. It is worth noting that ActionCable itself has inherent concurrency limits that apply regardless of which backend adapter is used. At very high connection counts, the bottleneck shifts to ActionCable, not to SolidCable or Redis.
With SQLite:
- 100 concurrent connections: Comparable to Redis
- 250 concurrent connections: Slight latency increase
- 500 concurrent connections: Noticeable latency, still functional
- 750 concurrent connections: Consider Redis or AnyCable
With PostgreSQL (using LISTEN/NOTIFY):
- 100 concurrent connections: Comparable to Redis
- 500 concurrent connections: Comparable to Redis
- 1000 concurrent connections: Slight latency increase
- 2000 concurrent connections: Consider AnyCable for this scale
PostgreSQL with LISTEN/NOTIFY performs significantly better
at higher connection counts due to push-based notifications
instead of polling.
The gap between SolidCable and Redis narrows considerably
when using the PostgreSQL adapter.
When to Use SolidCable
SolidCable is ideal when:
- We want to eliminate Redis dependency
- Latency requirements are not extreme
- We prefer simpler infrastructure
Consider alternatives when:
- We need sub 10 millisecond message delivery
- We already use Redis for other features
For very high concurrency (thousands of connections), neither SolidCable nor the Redis adapter will help much. ActionCable itself becomes the bottleneck at that scale. Consider AnyCable for such use cases, which offloads WebSocket handling to a Go server for significantly better performance and lower memory usage.
Migration from Redis
Migrating from Redis adapter is straightforward:
- Install SolidCable
- Configure the cable database
- Update
config/cable.yml - Deploy
# Before
production:
adapter: redis
url: <%= ENV["REDIS_URL"] %>
# After
production:
adapter: solid_cable
connects_to:
database:
writing: cable
polling_interval: 0.1.secondsExisting WebSocket connections will disconnect during deployment. Clients will automatically reconnect.
Conclusion
SolidCable brings database backed WebSocket support to Rails 8, eliminating Redis as a requirement for real time features. It integrates seamlessly with ActionCable and Turbo Streams, making it easy to add real time functionality to the application.
The polling based approach introduces some latency compared to Redis pub/sub, but for most applications, the tradeoff is worthwhile. Simpler infrastructure means fewer things to manage and monitor.
Check the official SolidCable repository for the latest documentation and benchmarks.
