Custom Turbo Stream Actions in Rails (Redirect view to new url from Background Jobs)

Edit
equivalent Web Development
Public
Rails
hotwire
turbo


The Problem

You have a controller action that's too slow (timeouts). You want to move the work to a background job, show a spinner immediately, and redirect the user when the job finishes.

What You Need

1. Register a custom Turbo Stream action on the client

In your JavaScript entrypoint (e.g. app/javascript/application.js):

import * as Turbo from "@hotwired/turbo"

Turbo.StreamActions.redirect = function () {
Turbo.visit(this.target)
}

This tells Turbo: when you see <turbo-stream action="redirect" target="/some/path">, call Turbo.visit() on that path. Works for streams arriving via HTTP response or ActionCable broadcast.

2. Add a server-side helper to generate the tag

Create app/helpers/turbo_stream/actions_helper.rb:

module TurboStream::ActionsHelper
def redirect(url)
turbo_stream_action_tag :redirect, target: url
end
end

Turbo::Streams::TagBuilder.include(TurboStream::ActionsHelper)

This lets you write turbo_stream.redirect(url) in views/controllers. Optional — you can also use turbo_stream.action(:redirect, url) without this helper.

3. Subscribe the view to a broadcast channel

In the view where the user will be waiting, add a turbo_stream_from tag. This opens an ActionCable WebSocket subscription:

<%= turbo_stream_from @record, :my_channel %>

Important: This tag must be inside the active Turbo Frame if your app uses frame-based navigation. If it's outside the frame, it won't be connected when the page loads via frame navigation.

4. Add a target element for the spinner replacement

Wrap the button/area you want to replace with a targetable element:

<div id="processing_indicator">
<%= button_to "Do the thing", my_path, method: :patch %>
</div>

5. Create the background job

The job does the real work and broadcasts the result:

class MyJob < ApplicationJob
include Rails.application.routes.url_helpers

def perform(record_id, user_id)
record = MyRecord.find(record_id)
user = User.find(user_id)

if MyService.call(record, user)
Turbo::StreamsChannel.broadcast_action_to(
[record, :my_channel], # must match turbo_stream_from
action: :redirect,
target: success_path(record) # the URL to redirect to
)
else
Turbo::StreamsChannel.broadcast_replace_to(
[record, :my_channel],
target: "processing_indicator",
html: '<div id="processing_indicator">Something went wrong.</div>'
)
end
end
end

The stream name ([record, :my_channel]) must match what turbo_stream_from subscribes to.

6. Update the controller to enqueue the job

def my_action
MyJob.perform_later(@record.id, current_user.id)

respond_to do |format|
format.turbo_stream # renders the spinner template
format.html { redirect_to fallback_path, notice: "Processing..." }
end
end

7. Create the turbo_stream response template

app/views/my_controller/my_action.turbo_stream.erb:

<%= turbo_stream.replace "processing_indicator" do %>
<div id="processing_indicator">
<svg class="spinner">...</svg>
</div>
<% end %>

The Flow

User clicks button
→ Controller enqueues job, returns turbo_stream
→ turbo_stream replaces button with spinner
→ Page is subscribed to ActionCable channel via turbo_stream_from
→ Job runs in Sidekiq
→ Job broadcasts <turbo-stream action="redirect" target="/success/path">
→ Client-side Turbo.StreamActions.redirect calls Turbo.visit("/success/path")
→ User sees the success page

Prerequisites

  • turbo-rails gem (standard in Rails 7+)
  • ActionCable configured and running (Redis adapter for production)
  • Sidekiq or another ActiveJob backend for background jobs

Gotchas

  • turbo_stream_from must be inside the active Turbo Frame if you use frame-based navigation, otherwise the subscription is never established
  • Pass primitive IDs to jobs, not ActiveRecord objects — avoids serialization issues
  • broadcast_action_to accepts any action: value — it's not limited to Turbo's built-in actions (append, replace, etc.)
  • The custom action name must match exactly between Turbo.StreamActions.your_action (JS) and action: :your_action (Ruby)

Payment successful

Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore.