Introduction
Rails Hotwire Turbo uses ActionCable to broadcast real-time updates via Turbo::StreamsChannel. When the channel is misconfigured, the broadcast silently fails, or the frontend does not receive updates, users see stale content without any error indication. This is especially problematic for notification systems, live dashboards, and collaborative features where real-time updates are core to the user experience.
Symptoms
- Turbo stream broadcasts do not update the page
- ActionCable logs show
Turbo::StreamsChannelsubscription errors - Database records update but UI does not reflect changes
turbo_stream_fromhelper renders but no WebSocket messages arrive- No errors in browser console or Rails logs
Check Turbo broadcast in Rails console:
``ruby
# Test broadcast manually
Turbo::StreamsChannel.broadcast_update_to(
"posts",
target: "post_123",
partial: "posts/post",
locals: { post: Post.find(123) }
)
# Returns nil if successful, but page does not update
Common Causes
- ActionCable not properly connected on the frontend
- Turbo::StreamsChannel not subscribed in JavaScript
- Broadcast to wrong stream name (singular vs plural)
- Missing
turbo-railsgem or incorrect version - Cable adapter misconfigured in production
Step-by-Step Fix
- 1.Verify ActionCable frontend connection:
- 2.```javascript
- 3.// app/javascript/controllers/application.js
- 4.import { Application } from "@hotwired/stimulus"
- 5.import { Turbo } from "@hotwired/turbo-rails"
// Ensure Turbo is connected to ActionCable // app/javascript/application.js import "@hotwired/turbo-rails" import "./controllers"
// app/javascript/channels/index.js // Load all channels from app/javascript/channels/ import { createConsumer } from "@rails/actioncable"
export default createConsumer() ```
- 1.Fix stream name matching in views and broadcasts:
- 2.```erb
- 3.<!-- WRONG - mismatched stream names -->
- 4.<!-- In view -->
- 5.<%= turbo_stream_from @user, "notifications" %>
<!-- In controller (wrong stream name) --> Turbo::StreamsChannel.broadcast_append_to( "user_notifications", # Does not match! target: "notifications", partial: "notifications/notification" )
<!-- CORRECT - matching stream names --> <!-- In view --> <%= turbo_stream_from @user, "notifications" %>
<!-- In controller --> Turbo::StreamsChannel.broadcast_append_to( [@user, "notifications"], # Matches turbo_stream_from target: "notifications", partial: "notifications/notification" ) ```
- 1.Verify Cable configuration in production:
- 2.```ruby
- 3.# config/cable.yml
- 4.production:
- 5.adapter: redis
- 6.url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
- 7.channel_prefix: myapp_production
# NOT this (in-memory adapter does not work across processes): # production: # adapter: async ```
- 1.**Debug Turbo stream broadcasts with logging":
- 2.```ruby
- 3.# config/initializers/turbo_logging.rb
- 4.module Turbo
- 5.module Broadcastable
- 6.extend ActiveSupport::Concern
included do after_commit -> { log_turbo_broadcast }, on: [:create, :update, :destroy] end
private
def log_turbo_broadcast Rails.logger.info( "[Turbo] Broadcasting for #{self.class}##{id}: " \ "action=#{persisted? ? 'update' : 'create'}" ) end end end ```
- 1.Test Turbo streams in development with Browser console:
- 2.```javascript
- 3.// In browser DevTools console
- 4.// Check ActionCable connection
- 5.Turbo.session.navigator.connection
// Check if subscribed to stream document.querySelector('[data-turbo-stream-source]')
// Manually trigger a turbo stream update
Turbo.renderStreamMessage(
<turbo-stream action="append" target="notifications">
<template><div>New notification</div></template>
</turbo-stream>
)
```
Prevention
- Always use model-based stream names (
turbo_stream_from @post) rather than strings - Test real-time updates in integration tests with
Turbo::StreamsChannel - Monitor ActionCable connection count in production metrics
- Use
turbo-railsgem version matching your Rails version - Add a heartbeat or connection status indicator to the UI
- Document stream naming conventions for the team