Turbo Streams: Push Partial HTML Updates Over WebSocket or SSE
Turbo Streams let a Rails app push small HTML fragments to the browser over WebSocket or Server‑Sent Events, updating only the parts of the page that need it. This guide shows the essential code, explains how the browser processes streams, and lists limits and common pitfalls.
10 Oct 2025, 11:38 UTC

What Turbo Streams Do
Turbo Streams are a Hotwire feature that allows the server to push small HTML fragments to the browser over a persistent connection (WebSocket or Server‑Sent Events). When the browser receives a <turbo-stream> element, it parses the action attribute and applies that change to the specified target element in the current document. The result is a real‑time UI update without a full page reload or any custom JavaScript.
Key Takeaway
Use Turbo Streams when you need to update a specific part of the page (e.g., a chat window, notifications list) in real time, keeping the server side simple and the client side free of custom JS.
How It Works – A Worked Configuration
Below is a minimal Rails example that demonstrates a controller action sending a Turbo Stream to append a new message to a chat room.
1. Enable the Turbo Streams Channel
In config/routes.rb mount the Turbo Streams channel. Rails already provides a default channel at /cable for ActionCable, which Turbo Streams use.
Rails.application.routes.draw do
mount ActionCable.server => '/cable'
# other routes
end
2. Define the Controller Action
When a user posts a new message, the controller renders a Turbo Stream response. The format.turbo_stream block sends a single <turbo-stream> element that instructs the browser to append the rendered partial to the target container.
class MessagesController < ApplicationController
def create
@message = Message.create!(message_params)
respond_to do |format|
format.html { redirect_to chat_path }
format.turbo_stream
end
end
private
def message_params
params.require(:message).permit(:content, :user_id)
end
end
3. Render the Turbo Stream
In app/views/messages/create.turbo_stream.erb (or inside the controller using render turbo_stream:), produce the stream element:
<%= render partial: "messages/message", locals: { message: @message } %>
When the browser receives this, it will locate the element with id="messages" and append the rendered message partial inside it.
4. Verify the Stream in the Browser
- Open DevTools, go to the Network tab, filter for
WSorSSErequests. - Locate the
text/vnd.turbo-stream.htmlresponse; it should contain the<turbo-stream>element. - Check the Elements tab: the target container updates without a full reload.
Limits and Practical Constraints
- Single Action per Element: A
<turbo-stream>can contain only one action (e.g.,append,replace,remove). For complex updates, send multiple streams. - Well‑Formed HTML Required: The server must emit valid
<turbo-stream>markup. Malformed streams are silently ignored. - Payload Size: Large fragments increase latency and consume bandwidth. Keep each stream small.
- Same‑Origin Only: Streams work only over WebSocket/SSE connections that share the same origin as the page, for security and CORS reasons.
- Authentication: Ensure the connection is authenticated (e.g., via ActionCable's
connection_params) to prevent unauthorized UI manipulation.
Common Mistakes to Avoid
- Forgetting the
text/vnd.turbo-stream.htmlMIME type. Rails sets this automatically when you useformat.turbo_stream, but a custom endpoint must set it manually. - Using an action on a target ID that does not exist in the current DOM; the stream will be dropped without error.
- Mixing Turbo Streams with manual DOM manipulation (e.g., jQuery
$('#messages').append(...)). Conflicting updates can cause race conditions. - Sending streams that modify elements outside the current document scope (e.g., a modal that is not present); Turbo ignores such streams.
- Relying on Turbo Streams for critical business logic. They are a presentation layer; always validate state on the server side.
Practical Checklist Before Deployment
- Confirm the WebSocket/SSE endpoint is served over
wss://orhttps://to avoid mixed‑content blocks. - Test that the stream arrives in the Network tab and that the target element updates.
- Verify that disabling JavaScript or blocking the WebSocket stops the update, proving dependency on Turbo Streams.
- Measure payload size; keep fragments under a few kilobytes for optimal performance.
- Audit the target IDs used in streams to ensure they exist in every relevant view.
When Turbo Streams Are the Right Choice
If your application requires frequent, small UI updates that are tied to server events—such as chat messages, live notifications, or collaborative document edits—Turbo Streams provide a lightweight, Rails‑native solution. They eliminate the need for custom JavaScript frameworks, reduce bundle size, and keep the codebase DRY.
Alternatives and When to Skip Turbo Streams
- For complex stateful interactions that involve significant client‑side logic, consider a dedicated front‑end framework (React, Vue) with a GraphQL or REST API.
- If you need cross‑origin streaming or support for legacy browsers that lack WebSocket, SSE may be insufficient; fallback to polling.
- When you must guarantee delivery of every update regardless of network conditions, implement a retry or acknowledgment mechanism on top of Turbo Streams.
Conclusion
Turbo Streams turn the server into a real‑time UI engine, delivering just enough HTML to update the browser efficiently. By following the example above, respecting the limits, and avoiding common pitfalls, you can add instant, low‑latency updates to a Rails app without writing custom JavaScript.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.