Rails 8.1

Rails 8.1: what changed, and should you upgrade yet?

Job continuations, structured events, local CI, Markdown rendering, deprecated associations and registry-free Kamal deploys — what’s worth adopting, and the short path from 8.0.

ANAbhay Nikam, Founder, Navio Labs

Rails 8.0 was the big infrastructure release. Rails 8.1, released on October 22, 2025, is the opposite: a set of practical tools you’ll reach for in ordinary weeks. None of them change how you build a Rails app. Several of them remove an annoyance you’ve probably learned to live with.

Here’s what landed, which parts we actually use, and how small the upgrade is.

Active Job continuations

The problem: a long job — importing a big CSV, backfilling a column, sending a newsletter — gets killed halfway through a deploy and starts again from zero. With Kamal, job containers get about 30 seconds to shut down by default, so this happens more than you’d think.

The fix: include ActiveJob::Continuable and split the work into steps. If the job is interrupted, it resumes from the last completed step, and steps can keep a cursor so they resume mid-loop.

class ImportContactsJob < ApplicationJob
  include ActiveJob::Continuable

  def perform(import_id)
    @import = Import.find(import_id)

    step :prepare do
      @import.update!(status: "running")
    end

    step :rows do |step|
      @import.rows.find_each(start: step.cursor) do |row|
        row.create_contact!
        step.advance! from: row.id
      end
    end

    step :finish
  end

  private

  def finish
    @import.update!(status: "done")
  end
end

There are three forms: a block (step :prepare do ... end), a block that receives the step so it can track a cursor, and a bare symbol that calls a private method of the same name.

Our take: this is the headline feature for us. Any job that can run longer than a deploy window should be continuable. It’s a bigger win than it looks.

Structured event reporting

Rails now has a single API for emitting structured events, instead of mixing Rails.logger lines, ActiveSupport::Notifications and whatever your APM wants.

Rails.event.notify("user.signup", user_id: user.id, plan: user.plan)

Rails.event.tagged("checkout") do
  Rails.event.notify("payment.captured", amount_cents: 4_900)
end

Rails.event.set_context(request_id: request.request_id)

Subscribers implement #emit(event) and receive a hash with the event’s name, payload and source location, so shipping events to your log pipeline or analytics tool is a small class.

Our take: worth adopting for new instrumentation. Don’t rewrite existing logging just to use it.

Local CI with bin/ci

New apps get config/ci.rb and a bin/ci script. You describe your pipeline in a small Ruby DSL and run it on your own machine:

# config/ci.rb
CI.run do
  step "Setup", "bin/setup --skip-server"
  step "Style: Ruby", "bin/rubocop"
  step "Security: Brakeman", "bin/brakeman --quiet --no-pager"
  step "Tests: Rails", "bin/rails test"

  if success?
    step "Signoff: All systems go", "gh signoff"
  else
    failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
  end
end

The optional gh signoff step uses the GitHub CLI and the basecamp/gh-signoff extension to mark a commit as passing, so a PR can require a local CI run before merge.

Our take: for small teams with fast suites, this genuinely replaces a hosted CI bill. For bigger teams, it’s still a nice single source of truth for “what does CI run?” even if the cloud runs it.

Markdown rendering

Markdown is now a first-class response format. Define to_markdown on a model and respond to it like any other format:

class PagesController < ApplicationController
  def show
    @page = Page.find(params[:id])

    respond_to do |format|
      format.html
      format.md { render markdown: @page }
    end
  end
end

Our take: this matters more than it sounds. Serving clean Markdown is the easiest way to make your content readable by AI tools and agents. If you have docs, help articles or a knowledge base in Rails, add it.

Fetching credentials from the command line

bin/rails credentials:fetch reads a single value from your encrypted credentials. The obvious use is Kamal’s secrets file:

# .kamal/secrets
KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)

You only need the master key, so it’s a lightweight alternative to wiring up a password manager for small projects.

Deprecated associations

You can now mark an association as deprecated and get told everywhere it’s still used — direct calls, preload, nested attributes and so on:

class Account < ApplicationRecord
  has_many :legacy_invoices, deprecated: true
end

The default mode warns; you can also choose :raise or :notify. This is the tool we wish we’d had on every “can we delete this table yet?” conversation.

Registry-free Kamal deploys

With Kamal 2.8, simple deployments can use a local registry by default, so you no longer need a Docker Hub or GHCR account to get started. Bigger setups should keep a remote registry.

The upgrade from 8.0 to 8.1

This one is short. The upgrade guide lists a single notable change: schema.rb now sorts table columns alphabetically by default. It makes schema dumps identical across machines and kills a whole class of noisy diffs. Expect one big, harmless diff in db/schema.rb the first time you run migrations — commit it on its own. If you truly need column order preserved, switch to structure.sql.

The rest is the usual routine:

  1. Make sure you’re on the latest 8.0.x and deprecation warnings are clean. (Ruby 3.2+ is still the minimum.)
  2. Bump gem "rails", "~> 8.1.0" and run bundle update rails.
  3. Run bin/rails app:update, review each diff, and keep load_defaults 8.0 for the first deploy.
  4. Run tests, deploy, then flip new_framework_defaults_8_1.rb settings one at a time.
  5. Change to load_defaults 8.1 and delete the defaults file.

Coming from 7.2? Do the 7.2 → 8.0 upgrade first and deploy it. Don’t jump two minor versions in one go.

Should you upgrade yet?

Yes, if you’re on 8.0. It’s a small upgrade with real quality-of-life wins, and job continuations alone are worth it if you deploy often. Check that your key gems support 8.1, then schedule it like any other small PR.

If you’d rather have someone handle the upgrade with you, that’s what we do.

Sources

More blogs

Rather have someone do this with you?

That’s what we do. Tell us where you’re stuck and you’ll hear back from the founder within one business day.

Start a project with Navio Labs
Get help with this