Rails 7.2 to 8.1 Upgrade: What Actually Breaks and How to Fix It

Everything that breaks upgrading Rails 7.2 to 8.1, and how to fix it: enum syntax, the Solid trifecta, Propshaft, params.expect, and every silent regression.

Ally Piechowski · · 9 min read
Rails 7.2 to 8.1 Upgrade: What Actually Breaks and How to Fix It

The Rails 7.2 to 8.1 upgrade is one of the smoothest major version jumps in recent Rails history. Fewer hard removals, fewer gotchas that prevent the app from booting.

The harder part is the ecosystem shift. Rails 8 wants you to drop Redis, drop Sprockets, drop Sidekiq, and adopt a full stack of database-backed infrastructure. None of that is mandatory. But it’s the direction the framework is moving, and the gap widens with every minor release.


Before You Touch the Gemfile

Upgrade Ruby First

Rails 8.0 requires Ruby 3.2 minimum, and 3.4 is recommended for 8.1.

Rails Version Minimum Ruby Recommended
7.2 3.1 3.2+
8.0 3.2 3.3+
8.1 3.2 3.4+

If you’re going to Ruby 3.4, two things to watch:

csv was removed from Ruby’s standard library. If anything in your app or gems uses CSV parsing, add gem "csv" to your Gemfile explicitly. This surfaces as a confusing LoadError that doesn’t mention the Ruby version change.

it is now a reserved keyword in Ruby 3.4 (the default block parameter, like _1 but named). Any code using it as a block-local variable name will silently change behavior. RSpec’s it method is fine since it’s a method call, not a variable, but custom helpers or code inside spec blocks that use it as a local variable will bite you.

Fix Your Deprecation Warnings

On your Rails 7.2 app:

# config/environments/test.rb
Rails.application.deprecators.behavior = :raise

Run your full test suite. The common ones you’ll hit:

These were warnings in 7.2. They’re NoMethodError or ArgumentError in 8.0.

Check Gem Compatibility

bundle outdated
bundle exec bundle_report compatibility --rails-version=8.1
bundle exec bundle-audit check --update  # security vulnerabilities

If you’re doing a broader codebase audit alongside the upgrade, here’s how I approach the Gemfile.

Gems that might break:


What Breaks in the Rails 7.2 to 8.1 Upgrade

The Enum Syntax Change

This is the one that bites the most codebases. Rails 8 removes the deprecated keyword argument syntax for enums:

# Before (removed in Rails 8):
enum status: { active: 0, archived: 1 }
enum status: { active: 0, archived: 1 }, _prefix: true

# After:
enum :status, { active: 0, archived: 1 }
enum :status, { active: 0, archived: 1 }, prefix: true

The first positional argument is now the enum name. The second is the mapping. Options like _prefix, _suffix, and _default drop their underscores.

If you have a lot of enums, this is the busywork of the upgrade. A find-and-replace gets you 90% of the way:

grep -rn "enum.*:" app/models/

Regexp.timeout Default

Rails 8 sets Regexp.timeout to 1 second by default for ReDoS protection. Unlikely to hit most apps, but if you do complex regex matching against user input, test your edge cases; they’ll raise Regexp::TimeoutError instead of hanging.

Removals

Rails Console Customizations

rails/console/app, rails/console/helpers, and extending the console via Rails::ConsoleMethods are all removed in Rails 8.0. If you have console helpers in an initializer or a custom console setup, they’ll silently stop loading.

Move any console helpers to a plain Ruby module and include it manually, or use the console block in config/environments/development.rb:

console do
  include MyConsoleHelpers
end

to_time Now Always Preserves Timezone

config.active_support.to_time_preserves_timezone is deprecated in Rails 8.1. The legacy false option is removed, and TimeWithZone#to_time now always preserves the receiver’s named timezone:

# Old behavior (to_time_preserves_timezone = false):
time_with_zone.to_time
# => converted to system local time

# New enforced behavior:
time_with_zone.to_time
# => preserves the named timezone

If your app has config.active_support.to_time_preserves_timezone = false, remove it and test .to_time calls on TimeWithZone objects. They’ll now preserve the named timezone rather than converting to system local.

Leading Brackets in Parameter Names

[foo]=bar in a query string used to parse as {"foo" => "bar"}. In Rails 8.1 it parses as {"[foo]" => "bar"}. If any API clients or forms submit parameter names starting with [, they’ll silently break: params just won’t match what the controller expects.

ActionText Rendering Change

ActionText now renders video attachments as <action-text-attachment> elements instead of <video> tags. If your tests assert on specific HTML output from ActionText, they may break. Check for content-type attributes instead of element types.


What’s New in Rails 8.1

params.expect

Rails 8 introduces params.expect as the preferred replacement for params.require(:foo).permit(:bar). The old syntax still works (it’s not removed), but new Rails generators use expect, and it’s stricter about parameter structure.

# Before:
params.require(:post).permit(:title, :body, comments: [:message])

# After:
params.expect(post: [:title, :body, comments: [[:message]]])

params.expect validates the shape of the parameters, not just their presence. If someone sends post=123 instead of post[title]=foo, require would raise but expect handles this more consistently.

The double-array on comments isn’t a typo. [:message] means “a hash with a message key.” [[:message]] means “an array of hashes each with a message key.” params.expect requires you to be explicit about which you expect. Get it wrong and you’ll silently permit nothing.

params.expect returns an ActionController::Parameters object, same as permit. It’s designed to replace the full require(...).permit(...) chain, not be chained onto.

For new controllers, use expect. No need to migrate old ones all at once.

Propshaft Is the Default Asset Pipeline

New Rails 8 apps get Propshaft instead of Sprockets. Existing apps keep Sprockets.

If you want to move to Propshaft, I wrote a full migration guide.

If your pipeline is stable, keep it. Add gem "sprockets-rails" to your Gemfile and move on.

The Solid Trifecta

Rails 8’s headline feature is database-backed replacements for Redis:

Redis was an unnecessary ops dependency for most apps. Glad to see it become optional.

Your Sidekiq and Redis setups still work; none of this is forced.

When to adopt: If your app doesn’t need Redis-level throughput (most don’t), the Solid stack removes an entire infrastructure dependency. Basecamp and HEY run this in production.

When to wait: If you’re processing 100k+ jobs per minute or need sub-millisecond cache reads, stick with Redis. SSDs are fast, RAM is faster.

Migration strategy: Don’t switch everything at once. Start with Solid Queue for non-critical jobs, monitor, then move cache. Cable last, if at all.

The Authentication Generator

bin/rails generate authentication creates a session-based auth system with password resets. This is a starting point for new apps, not a replacement for Devise.

If you already have Devise, ignore it entirely.

Schema Column Reordering

Active Record now sorts table columns alphabetically in schema.rb. Your first db:migrate after upgrading will produce a massive diff: every table’s columns get reordered.

This is purely cosmetic (it won’t change your database), but it will bloat your upgrade PR’s diff, cause merge conflicts with in-flight branches that touch migrations, and confuse reviewers.

Fix: Commit the schema.rb rewrite as its own isolated commit. Warn your team. In GitHub PR review, mark the schema.rb file as “Viewed” so reviewers can skip it. If you use structure.sql, column order is preserved; this only affects schema.rb.

Active Job Continuations

Long-running jobs can now be broken into steps that resume from the last completed step after interruption. Particularly useful with Kamal, which gives job containers 30 seconds to shut down during deploys.

Additive, doesn’t break existing jobs. Worth adopting if you have jobs that currently restart from scratch on every deploy.

Deprecated Associations

You can now mark associations as deprecated:

has_many :posts, deprecated: true
has_many :articles  # the replacement

Supports :warn, :raise, and :notify modes. Useful for large codebases where you’re migrating off a legacy association and want to track usage.

Deprecations to Watch

These aren’t breaking yet, but they will be:


Rails 8.1 Upgrade Strategy

Phase 1: Preparation (on Rails 7.2)

  1. Upgrade Ruby to 3.2 minimum; 3.4 recommended.
  2. Set Rails.application.deprecators.behavior = :raise in config/environments/test.rb and fix every warning.
  3. Fix all enum definitions to use the new positional syntax.
  4. Add gem "sprockets-rails" to your Gemfile explicitly if you use Sprockets.
  5. Add gem "csv" to your Gemfile.

Phase 2: Dual Boot

# Gemfile
if next?
  gem "rails", "~> 8.1"
else
  gem "rails", "~> 7.2"
end

Run CI against both. Merge fixes to main continuously. The upgrade happens on main, not on a branch.

Phase 3: The Cutover

  1. Switch to Rails 8.1 as primary.
  2. Keep config.load_defaults "7.2".
  3. Run bin/rails db:schema:dump immediately. This triggers the schema column reordering before any real migrations run, so you can commit it in isolation. Do this before anyone on the team runs a migration.
  4. Deploy. Monitor.

Phase 4: Default Migration

Uncomment defaults from new_framework_defaults_8_1.rb one at a time. Each gets its own PR and deploy.

Phase 5: Infrastructure Migration (Optional, Separate)

Migrate from Redis to Solid Queue/Cache/Cable. Migrate from Sprockets to Propshaft. Don’t mix them with the framework upgrade.


Quick Reference

# Pre-upgrade checks (run on Rails 7.2)
bundle exec bundle_report compatibility --rails-version=8.1
bundle exec bundle-audit check --update
# railsdiff.org: diff framework defaults between 7.2 and 8.1
grep -rn "enum.*:" app/models/        # find old enum syntax
grep -rn "ProxyObject" app/            # removed in 8.0
grep -rn "read_encrypted_secrets" config/  # removed

# Post-upgrade essentials
bundle update rails
bin/rails app:update
bin/rails db:migrate
bin/rails test  # or: bundle exec rspec

# Check for Regexp timeout issues
grep -rn "Regexp\.\(new\|compile\)" app/ lib/

TL;DR

Breaking: enum syntax, to_time behavior, schema column reordering | New: Solid trifecta, params.expect, auth generator | Strategy: dual boot on main, not a branch

A Rails 7.2 to 8.1 upgrade for a medium-sized app is straightforward. The removed deprecations are mechanical to fix. You’ve already seen the warnings.

Where teams get stuck isn’t the framework upgrade itself. It’s the ecosystem migration. “Should we switch to Propshaft? Should we drop Sidekiq for Solid Queue? Should we rip out Redis?” Those are good questions, but they’re separate projects. Don’t let them block the version bump.

Get on Rails 8.1, get your defaults migrated, get security patches flowing again. Then decide what you want to adopt from the new stack at your own pace.

If that branch has been sitting untouched for months, get in touch. This is literally what I do.


Related Articles