Turbo Streams in Rails 7 – A Practical Implementation Guide
Learn how to add real‑time updates to a Rails 7 app with Turbo Streams. This guide covers prerequisites, a step‑by‑step setup, example code, and troubleshooting tips to help you get instant DOM updates without WebSockets.
02 Jul 2026, 12:06 UTC

What you’ll get
By the end of this article you’ll have a working Turbo Stream that pushes new messages to the page without a full reload. You’ll understand the minimal prerequisites, how to render a Turbo Stream response, how to subscribe to a stream in the view, and how to verify the connection.
Prerequisites
- Ruby 3.1+ and Rails 7.0+ installed.
- The
hotwire-railsgem added to your Gemfile and bundled. - JavaScript bundler (Webpacker, jsbundling‑rails, or importmaps) configured to include
turbo.js. - Basic knowledge of Rails routes, controllers, and ERB views.
- A browser that supports the EventSource API (most modern browsers).
Step 1 – Add Hotwire to the Gemfile
# Gemfile
gem "hotwire-rails", "~> 1.5"
Run bundle install and then rails hotwire:install. The install task will add turbo.js to your JavaScript pack and create a turbo_stream helper.
Check: Does turbo.js load?
Open any page that renders application.html.erb and inspect the page source. You should see a script tag pointing to turbo.js. If not, confirm the JavaScript pack path in config/webpacker.yml or your import map.
Step 2 – Create a stream endpoint
Define a route that will serve the stream. A common pattern is a collection action on a resource.
# config/routes.rb
Rails.application.routes.draw do
resources :messages do
collection do
get :stream, defaults: { format: :turbo_stream }
end
end
end
The defaults: { format: :turbo_stream } part tells Rails to set the MIME type text/vnd.turbo-stream.html automatically.
Check: Does the route exist?
Run rails routes | grep messages_stream. You should see a GET route to /messages/stream with format turbo_stream.
Step 3 – Controller action to push updates
In MessagesController add a create action that, on successful save, streams the new message to all subscribers.
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
def create
@message = Message.new(message_params)
if @message.save
respond_to do |format|
format.html { redirect_to messages_path, notice: "Message sent." }
format.turbo_stream {
render turbo_stream: turbo_stream.append(
"messages",
partial: "messages/message",
locals: { message: @message }
)
}
end
else
render :new
end
end
private
def message_params
params.require(:message).permit(:content)
end
end
Key points:
respond_toensures the action can reply with normal HTML or a Turbo Stream.- The
turbo_stream.appendhelper builds a<turbo-stream>tag that will be streamed to the client. - The partial
_message.html.erbshould render the new message markup.
Check: Does the action render the correct content?
From the Rails console, create a message and call app.messages.create(content: "test"). Then inspect response.body for the <turbo-stream> tag and the partial’s HTML.
Step 4 – Create the message partial
Place the following in app/views/messages/_message.html.erb:
<div id="message-<%= message.id %>" class="message">
<p><%= h message.content %></p>
</div>
Use h (alias for html_escape) to prevent XSS when rendering user‑supplied content.
Step 5 – Subscribe to the stream in the view
In the view that lists messages, add a Turbo Stream subscription. The simplest way is the helper turbo_stream_from:
<h1>Messages</h1>
<div id="messages">
<%= render @messages %>
</div>
<%= turbo_stream_from "messages/stream" %>
The turbo_stream_from helper emits a <turbo-stream> element with a src attribute pointing to the stream endpoint. Turbo automatically opens an EventSource connection and listens for incoming streams.
Check: Is the connection active?
Open the page in a browser, open the developer console, and look for a line similar to Turbo Stream: listening for events on /messages/stream. In the Network tab, you should see a persistent GET /messages/stream request with Content-Type: text/vnd.turbo-stream.html.
Step 6 – Test the real‑time update
Open the page in two browser windows. In one, submit a new message via the form. The other window should instantly display the new message without a full reload.
Common troubleshooting
- Stream not appearing – Verify that
turbo.jsis loaded and that theturbo_stream_fromtag exists. - Missing MIME type – Ensure the route defaults to
format: :turbo_streamor that the controller explicitly setsformat.turbo_stream. - Legacy browsers may not support SSE. In such cases, the connection will silently fail. Consider polyfills or fallback to WebSockets if you need broad support.
- Authentication: If the stream contains sensitive data, protect the endpoint with
before_action :authenticate_user!or similar.
Performance tips
- Limit the number of
turbo_stream_fromtags per page. Each tag opens a separate EventSource connection. - Scope updates to a
turbo_frame_tagwhen only a sub‑section of the page needs refreshing. - Use
turbo_stream.updateinstead ofappendfor large lists to avoid DOM reflow.
Recovery: Undoing a Turbo Stream
Turbo Streams are stateless on the client; they simply inject or modify DOM nodes. If you need to roll back a streamed update, send a new Turbo Stream with remove or replace actions targeting the same element IDs.
render turbo_stream: turbo_stream.remove("message-#{@message.id}")
Conclusion
Turbo Streams let Rails developers add real‑time features without the overhead of WebSockets. By following the steps above you can quickly wire up instant updates, verify the connection, and handle common pitfalls. Once in place, the rest of your UI can continue to use standard Rails conventions, keeping the stack simple and maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.