Designing Live Page Updates with Turbo Streams: An Architecture Note
An architecture note on using Turbo Streams for live Rails page updates: the minimal three-piece design, trust and authorization boundaries, operational checks, and the failure modes that would push you toward a client state layer.
03 Oct 2026, 05:39 UTC

You have a Rails page that shows data other people change — a task board, an order list, a comment thread — and you want it to update without a manual refresh. The useful takeaway: if your requirement is "push server-rendered HTML into a specific spot on the page," Turbo Streams is the smallest design that fits, and you should resist adding a client-side state layer until a concrete requirement forces one.
Requirements First
Write down what you actually need before choosing machinery. A typical honest list looks like this:
- Updates originate on the server (a record is created, updated, or destroyed).
- The change must appear on other users' open pages within a second or two.
- The update is a fragment of HTML — a new row, a replaced badge, a removed item — not a wholesale page redraw.
- The client does not need to mutate that data locally or reconcile conflicting edits.
If your list matches, Turbo Streams covers it. If it includes "users edit the same record concurrently and must see each other's keystrokes" or "the UI needs optimistic updates before the server confirms," you are in client-state territory and this design will fight you.
The Smallest Suitable Design
Turbo Streams deliver HTML fragments to the browser, either as an HTTP response to a form submission or pushed over a WebSocket via Action Cable. Each fragment carries an action (append, replace, remove, and a few others) and a target DOM id. The browser applies the fragment to the matching element. No application JavaScript is required for this path.
The minimal broadcast design has three pieces:
- A stream subscription on the page. In the view that should receive updates, add
<%= turbo_stream_from \"orders\" %>. This opens a WebSocket subscription to a named stream when the page loads. - A broadcast trigger. In the model,
after_create_commit { broadcast_append_to \"orders\" }, or callTurbo::StreamsChannel.broadcast_append_toexplicitly from a job if you want the render off the request path. - A partial. The broadcast renders
app/views/orders/_order.html.erbby convention and wraps it in a<turbo-stream>tag.
That is the whole architecture. The server remains the single source of truth; the browser is a dumb renderer of fragments.
Trust and Data Boundaries
The security model is simple but worth stating explicitly: Turbo trusts server-rendered HTML completely and inserts it into the DOM. Two boundaries follow from that.
Output boundary. Never interpolate raw user input into a broadcast partial. Use the normal Rails escaping helpers (<%= %> escapes by default; raw and html_safe bypass it). A practical audit: render the partial in a test and assert the response contains no <script> tags for user-controlled fields.
Authorization boundary. Stream names are just strings. Any client can subscribe to turbo_stream_from \"orders\" unless you scope it. For per-user or per-account data, include the scope in the stream name and verify it in the channel: turbo_stream_from current_account, :orders, and in ApplicationCable::Connection authenticate the user and reject connections that should not exist. Broadcasting a private record to a public stream name leaks it to every subscriber — this is the most common real-world mistake with this feature.
Operational Checks
Before and after shipping, verify these in a running app (development is fine; you need a browser and server logs):
- Fragment validity. Trigger a broadcast and inspect the rendered partial's response body. It must be well-formed HTML with a unique
idmatching the stream target (e.g.,dom_id(order)). A mismatched id means the fragment silently does nothing. - DOM application. Open two browser sessions, create a record in one, and confirm the fragment appears in the other without a refresh.
- Log cleanliness. Watch the Rails log during normal create/update/destroy cycles. You should see the broadcast and partial render with no exceptions.
- Connection behavior. Throttle the network in browser dev tools or kill the connection briefly; confirm the page resubscribes and that no stale or duplicated nodes accumulate after reconnect.
Failure Modes
- Client disconnects mid-delivery. The server may log a broken-pipe-style error when a subscriber vanishes during a write. This is usually noise, but a spike in these errors alongside deploys points to connections being dropped rather than drained.
- Partial or malformed HTML. On unreliable networks, a mangled fragment can corrupt layout — a half-replaced list, a duplicated row. Recovery generally requires a client refresh; Turbo will not reconcile the DOM for you. Mitigate by keeping fragments small and idempotent: prefer
replaceoverappendwhere duplication would be visible. - Missed broadcasts. WebSocket delivery is not a queue. A client that was offline during the broadcast simply never gets it. If missed updates are unacceptable, add a staleness check — for example, refresh on
visibilitychangeafter a long background period. - N+1 renders. Broadcasting from a model callback inside a loop renders the partial per record. Move bulk work to a background job and broadcast a single collection replace instead.
Conditions That Would Change the Design
Revisit this architecture when any of these become true:
- Client-side mutation is required. If users must edit data locally and see immediate optimistic feedback, server-rendered fragments are the wrong tool; you need a client state layer and conflict handling.
- High-frequency updates. Streams rendering partials per event scale poorly past a few updates per second per client. At that point, batch updates or push data (not HTML) and render once.
- Delivery guarantees matter. If every update must reach every client, add a catch-up mechanism — a version counter on the page and a refetch when the client detects a gap.
Until one of those conditions arrives, the three-piece design — subscription, broadcast, partial — is the right size. The discipline that keeps it working is respecting the two boundaries: escape everything user-controlled, and scope every stream to its authorized audience.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.