ruby erb 95 lines · 4 tabs

Live Turbo Streams Progress Bar for a Long Rails Job

Shared by codesnips Jul 2026
4 tabs
class ImportRun < ApplicationRecord
  enum status: { pending: 0, running: 1, completed: 2, failed: 3 }

  def percent
    return 0 if total.to_i.zero?
    [(processed.to_f / total * 100).round, 100].min
  end

  def bump!(by = 1)
    increment!(:processed, by)
    self
  end

  def broadcast_progress
    broadcast_replace_to(
      self,
      target: ActionView::RecordIdentifier.dom_id(self),
      partial: "import_runs/progress",
      locals: { run: self }
    )
  end
end
4 files · ruby, erb Explain with highlit

This snippet shows how a long-running background job can push a live progress bar to the browser using Turbo Streams, without writing any custom JavaScript for the updates. The pattern relies on three collaborating pieces: an ImportRun model that persists progress as durable state, an ImportJob that does the work and broadcasts incremental updates, and a _progress partial that Turbo replaces in place on every update.

In ImportRun model, progress is treated as a first-class attribute rather than something ephemeral in memory. The record holds total, processed, and status columns, and percent derives a bounded integer so the view stays dumb. Storing progress in the database matters because a browser can disconnect and reconnect mid-import; when the page reloads it renders the current percent straight from the row instead of losing the bar. The bump! method advances processed and returns the run so the job can chain a broadcast, while broadcast_progress re-renders the partial and pushes it over the stream named after the record via broadcast_replace_to.

The ImportJob is where the work and the throttling live. GlobalID-backed arguments let Active Job serialize the ImportRun directly. It sets total, marks the run running, then iterates the source rows. A key detail is that it does not broadcast on every single row — broadcasting thousands of times per second would flood Action Cable and jank the UI — so it only calls broadcast_progress every 25 rows and once at the end. The after_perform and rescue_from callbacks guarantee a terminal broadcast: success flips the run to completed, and any exception flips it to failed before re-raising, so the bar never gets stuck at an ambiguous state.

The _progress partial is an ordinary ERB fragment wrapped in a turbo_frame-style DOM id (dom_id(run)) that matches the broadcast target. Its aria-valuenow and width style are computed from percent, giving an accessible, CSS-driven bar. Because broadcast_replace_to targets that same id, each broadcast swaps the whole fragment atomically.

The main trade-off is that progress accuracy depends on total being known up front; for unbounded streams an indeterminate bar or a periodic timer broadcast is a better fit. This approach is ideal when the total count is cheap to compute and users need reassurance a slow import is actually moving.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Live Turbo Streams Progress Bar for a Long Rails Job — share card
Link copied