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
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:
enumdefined with keyword arguments → use positional hash syntaxActiveRecord::ConnectionAdapters::ConnectionPool#connection→ use#with_connectionor#lease_connectionconfig.active_record.commit_transaction_on_non_local_return→ removed in 8.0, decide your behavior nowActiveSupport::ProxyObject→ gone, useBasicObjectdirectly- Passing
niltoform_with model:→ use an explicit empty object or omit the argument
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:
- Devise: check their Rails 8.1 compatibility status. Version 5.x is current; older versions have known issues. Watch for
status: :unprocessable_entitybecomingstatus: :unprocessable_content. - ActiveAdmin: still catching up. Check their Rails 8 compatibility issues before committing.
- acts-as-taggable-on: has known Rails 8.1 compatibility issues; check their issues page. You may need to pin to a GitHub ref until a release ships.
- SimpleCov: older versions throw
REXML::ParseExceptionon Rails 8. Update to latest. - WebMock: older versions emit
Net::HTTPSession is deprecatedwarnings on Ruby 3.4. Update.
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
bin/rake stats: gone, usebin/rails statsBenchmark.ms: removed, useBenchmark.realtime- Azure storage service for Active Storage: removed
- Semicolons as query string separators: removed
- SQLite3 adapter
:retriesoption: deprecated in favor of:timeout
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:
- Solid Queue: background jobs, replaces Sidekiq/Resque
- Solid Cache: fragment caching, replaces Redis/Memcached
- Solid Cable: Action Cable pub/sub, replaces 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:
- Order-dependent finders like
.firstwithout.orderwill require explicit ordering in a future version String#mb_charsandActiveSupport::Multibyte::Charsare being removed- The built-in Sidekiq adapter is going away; Sidekiq 7+ ships its own adapter directly
Rails 8.1 Upgrade Strategy
Phase 1: Preparation (on Rails 7.2)
- Upgrade Ruby to 3.2 minimum; 3.4 recommended.
- Set
Rails.application.deprecators.behavior = :raiseinconfig/environments/test.rband fix every warning. - Fix all enum definitions to use the new positional syntax.
- Add
gem "sprockets-rails"to your Gemfile explicitly if you use Sprockets. - 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
- Switch to Rails 8.1 as primary.
- Keep
config.load_defaults "7.2". - Run
bin/rails db:schema:dumpimmediately. 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. - 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
- How I Audit a Legacy Rails Codebase in the First Week
- Migrating from Sprockets to Propshaft: Is It Worth It?
- Rails default_scope: Why You Should Never Use It
- Solved: Warning: Using the last argument as keyword parameters is deprecated
- Rails: How to Use Greater Than/Less Than in Active Record where Statements