Turbo Streams: Push Real‑Time Updates Without Full Reloads
Turbo Streams let servers push real‑time DOM updates over WebSocket or SSE, keeping the browser in sync without full reloads. Learn how to set up a Rails 7 chat example, weigh trade‑offs, and get started quickly.
13 Sept 2025, 10:04 UTC

Stale State in Modern Web Apps
When users interact with a single‑page or multi‑page app, the client often ends up out of sync with the server: a new record appears on the database, but the page still shows the old list. Traditional approaches involve a full page refresh or manual AJAX polling, both of which add latency and complexity.
Turbo Streams to the Rescue
Turbo Streams, part of Hotwire, solve this by letting the server push small DOM fragments to the browser over WebSocket or Server‑Sent Events (SSE). The browser receives a text/plain payload that contains a <turbo-stream> element; Turbo.js interprets the action and target attributes and applies the change to the existing page, preserving CSS, JavaScript bindings, and user scroll position.
How It Works in Rails 7+
- Include the
turbo-railsgem (or import Turbo.js directly). - Create an
ActionCablechannel that broadcastsTurbo::StreamsChannelmessages. - Emit a Turbo Stream from the server side:
Turbo::StreamsChannel.broadcast_to(user, turbo_stream.append("messages", partial: "messages/message", locals: { message: @message })). - On the client, a
<turbo-stream>tag in the layout listens for the channel and applies the update automatically.
Concrete Example: A Real‑Time Chat Box
Below is a minimal Rails 7 setup that demonstrates a chat message list updating instantly for all connected users.
# Gemfile
gem "turbo-rails"
# Run bundle install
bundle install
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat"
end
end
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
def create
@message = Message.create!(content: params[:content], user: current_user)
Turbo::StreamsChannel.broadcast_to("chat", turbo_stream.append("messages", partial: "messages/message", locals: { message: @message }))
head :ok
end
end
# app/views/layouts/application.html.erb
<body>
<turbo-frame id="messages"></turbo-frame>
<%= turbo_stream_from "chat" %>
</body>
# app/views/messages/_message.html.erb
<p># <%= message.user.name %>: <%= message.content %></p>
When a user submits a new message, the controller broadcasts a Turbo Stream that appends a new <p> element to the messages frame. All other clients receive the same stream and update their DOM instantly.
Trade‑Offs and Limitations
- Browser Support: Turbo Streams rely on WebSocket or SSE. Older browsers may fall back to long‑polling, increasing latency.
- Complex Client‑Side State: If a page contains heavy JavaScript widgets (e.g., React components) that are replaced by a stream, you may need to re‑initialize them manually.
- Payload Size: Sending large HTML fragments over WebSocket can inflate bandwidth. Consider batching or compressing updates for high‑frequency use cases.
- Overlapping Targets: When multiple streams attempt to modify the same DOM node, the order of application can lead to race conditions. Use unique target IDs or coordinate updates server‑side.
Actionable Next Steps
- Prototype a small feature (e.g., live comment counters) using Turbo Streams to gauge latency in your environment.
- Enable
ActionCablewith Redis or another adapter if you anticipate many concurrent users. - Monitor the network tab in DevTools: look for
turbo-streammessages and verifytext/plainMIME type. - For non‑Rails stacks, import Turbo.js via
import "@hotwired/turbo"and follow the same server‑side broadcast pattern.
By integrating Turbo Streams, you can keep the client state fresh with minimal code changes, improve perceived performance, and reduce the need for heavy client‑side frameworks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.