Introduction
Real-time updates have become table stakes for modern web applications. Whether you're building a chat application, a live notification feed, or a collaborative dashboard, users expect changes to appear instantly without page refreshes. Turbo Streams for live updates: best practices explained gives us a framework to push those changes directly to the page, and we'll walk through how to implement them correctly in a production Rails application.
Turbo Streams sit at the intersection of simplicity and power. Instead of polling the server, writing WebSocket handlers, or managing complex client-side state, we can broadcast DOM updates from the server and have them apply across connected clients in real time. In this guide, we'll cover the five core stream actions, how to trigger them from controllers, how models can broadcast changes automatically, and real-world patterns for chat, notifications, and dashboards.
Stream actions: the five primitives
Turbo Streams give us five core actions for modifying the DOM. Each action targets a specific element by its id attribute and transforms it in a predictable way. Understanding when to use each one is fundamental to building responsive features.
Append adds a new element as the last child of a container. This is your go-to action for chat messages, comment threads, or activity feeds where new items arrive at the bottom:
<turbo-stream action="append" target="messages">
<template>
<div id="message-42" class="message">
<strong>Alice:</strong> Just shipped the feature
</div>
</template>
</turbo-stream>Prepend adds a new element as the first child. Use this for notification banners or feeds where new items should appear at the top:
<turbo-stream action="prepend" target="notifications">
<template>
<div id="notification-789" class="alert alert-info">
Your order has shipped
</div>
</template>
</turbo-stream>Replace swaps an entire element for new content. This is useful when you need to update a record that's already displayed, such as changing an order status or updating a user's profile card:
<turbo-stream action="replace" target="order-42">
<template>
<div id="order-42" class="order-card">
<p>Status: <strong>Shipped</strong></p>
<p>Tracking: ABC123</p>
</div>
</template>
</turbo-stream>Update changes only the inner HTML of a target element, leaving the element itself intact. This is lighter weight than replace and useful for updating counts, status badges, or timestamps:
<turbo-stream action="update" target="comment-count">
<template>
47 comments
</template>
</turbo-stream>Remove deletes an element from the DOM. When a user deletes a message or you want to dismiss a notification, remove is the right choice:
<turbo-stream action="remove" target="message-42" />A single stream response can contain multiple actions, allowing you to orchestrate complex updates in one request. For example, you might append a new message, update a message count, and prepend a notification all at once.
Triggering streams from controllers
Controllers are the most direct place to send a Turbo Stream response. When an action completes—a message is created, an order status changes—you respond with the turbo_stream format instead of HTML or JSON.
Here's a chat application example. When a user posts a message, the controller broadcasts it to other users in the room and confirms delivery to the sender:
class MessagesController < ApplicationController
def create
@room = Room.find(params[:room_id])
@message = @room.messages.build(message_params)
@message.user = current_user
if @message.save
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.append("messages", @message)
end
end
else
render :new, status: :unprocessable_entity
end
end
private
def message_params
params.require(:message).permit(:body)
end
endThe key is render turbo_stream:, which accepts one or more stream actions. Notice that we pass the partial or model directly—Rails renders it as a view, automatically wrapping it with the target id="messages".
You can chain multiple actions together:
respond_to do |format|
format.turbo_stream do
render turbo_stream: [
turbo_stream.append("messages", @message),
turbo_stream.update("message_count", @room.messages.count),
turbo_stream.prepend("activity", @message.user.name + " posted")
]
end
endFor error cases, render the form with a 422 status. Turbo will replace the form on the client and display validation errors.
Broadcasting changes from models
Controllers handle immediate responses, but broadcasting keeps all subscribers in sync even when changes happen outside a direct request. When another user updates a record, or a background job completes, broadcasting ensures the update reaches connected clients.
Rails provides broadcast_append_to, broadcast_prepend_to, broadcast_replace_to, broadcast_update_to, and broadcast_remove_to callbacks. These use Action Cable under the hood to send updates to subscribed clients.
Here's a Message model that automatically broadcasts new messages to the room:
class Message < ApplicationRecord
belongs_to :room
belongs_to :user
after_create_commit -> { broadcast_append_to room }
endWhen a message is created and committed, it broadcasts an append action to all subscribers listening on the room stream. The partial rendered is messages/message.html.erb by convention, so make sure that partial exists:
<!-- app/views/messages/_message.html.erb -->
<div id="<%= dom_id(message) %>" class="message">
<strong><%= message.user.name %>:</strong>
<%= message.body %>
</div>For more complex scenarios, use custom blocks:
class Order < ApplicationRecord
after_update_commit -> {
broadcast_replace_to "orders:#{user_id}", partial: "orders/order", locals: { order: self }
}
endThe broadcast stream name uniquely identifies the set of subscribers. Use scoped names like "room:42" or "user:123:notifications" to ensure updates only reach the relevant clients.
On the view side, clients subscribe with the turbo-cable-stream-source tag:
<turbo-cable-stream-source channel="RoomChannel" room-id="<%= @room.id %>"></turbo-cable-stream-source>
<div id="messages">
<%= render @room.messages %>
</div>This tells Turbo to connect to the RoomChannel and listen for broadcasts. Rails will handle the Action Cable plumbing behind the scenes.
Real-world use cases
Chat applications are the canonical use case. Messages appear instantly, read receipts update without delay, and typing indicators can be sent as custom actions. The pattern is straightforward: append messages as they're created, remove them when deleted, and replace them if edited.
Notification systems benefit enormously from streams. When an event fires—a comment on a user's post, a mention in a message, an order shipped—prepend the notification to the inbox. Users see alerts without navigating away. Combine update actions to mark notifications as read and maintain unread counts.
Live dashboards use update actions to refresh charts, metrics, and tables. A sales dashboard might update conversion rates, revenue totals, and top-performing products as transactions arrive. Broadcasting from background jobs keeps the data fresh without API polling.
Collaborative editing can leverage streams for presence indicators, document updates, and conflict resolution feedback. When one user edits a shared resource, others see the change propagated in real time.
Inventory management uses streams to reflect stock changes across the warehouse. When inventory is allocated, received, or damaged, the counts update live so all users work with current data.
Common mistakes
Forgetting unique IDs is the most common pitfall. Every element you target must have a unique id attribute. Use Rails helpers like dom_id(record) to generate consistent IDs—dom_id(message) produces message_42 if the message has id: 42.
Broadcasting to overly broad streams sends updates to clients that don't need them. Use scoped stream names. Instead of broadcast_append_to "messages", use broadcast_append_to "room:#{room_id}" so only clients in that room receive the update.
Rendering heavy partials repeatedly can overload the server. If you're broadcasting updates for thousands of users, optimize the partial for rendering efficiency or consider pagination and virtual scrolling on the client.
Not handling errors gracefully leaves users confused when an action fails. Always respond with a turbo_stream for both success and failure cases, or use client-side error handling to inform users what went wrong.
Mixing Turbo Streams with page navigation can cause confusion. Streams update the current page; if you need to navigate elsewhere, use a redirect or form submission instead of a stream action.
Key takeaways
- Use append for new items in feeds, prepend for top-priority updates, replace for entire record changes, update for partial content, and remove for deletions.
- Respond with
turbo_streamformat in controllers to immediately push updates to the requesting client. - Broadcast from model callbacks using
after_commithooks to sync all connected subscribers when data changes. - Use scoped stream names (e.g.,
"room:42","user:123:notifications") to target updates precisely and avoid unnecessary network traffic. - Always include unique, stable
idattributes on elements you'll stream into; usedom_idfor consistency. - Chat, notifications, dashboards, and collaborative tools are natural fits for Turbo Streams; evaluate other features based on whether real-time updates improve user experience enough to justify the added complexity.






Comments
Loading comments…