Rails 5 brought major improvements: ActionCable for WebSockets, API mode, Turbolinks 5, and ActiveJob integration. But it also introduced breaking changes that require careful migration.
If we’re still on Rails 4.2 (EOL since 2016), this upgrade is critical for security and performance. Let’s walk through the key changes and how to handle them.
Note: This is Part 2 of our Rails Upgrade Series. Read Part 1: Planning Rails Upgrade for strategic planning guidance.
Before We Start
Expected Timeline: 2-4 weeks for medium-sized applications
Medium-sized application: 20,000-50,000 lines of code, 30-100 models, moderate test coverage, 2-5 developers. Smaller apps may take 1-2 weeks, larger enterprise apps 6-12 weeks.
Prerequisites:
- Test coverage of 80%+
- Ruby 2.2.2+ installed (Ruby 2.3+ recommended)
- Backup of production database
- Staging environment for testing
Ruby Version Requirements
Rails 5 requires Ruby 2.2.2 minimum, but we strongly recommend Ruby 2.3 or 2.4 for production.
Why Upgrade Ruby First?
Ruby 2.3 (December 2015) brings:
- Safe navigation operator (
&.) - Frozen string literal pragma
- Hash comparison
digmethod for nested access
Ruby 2.4 (December 2016) brings:
- Integer unification (Fixnum/Bignum → Integer)
- Performance improvements (5-10% faster)
- Better Unicode support
Ruby Upgrade Path
# Check current Ruby version
ruby -v
# Install Ruby 2.4 (using rbenv)
rbenv install 2.4.10
rbenv local 2.4.10
# Or using RVM
rvm install 2.4.10
rvm use 2.4.10
# Verify
ruby -v
# => ruby 2.4.10Important: Test the application with the new Ruby version before upgrading Rails.
Rails Upgrade Path
Step 1: Update the Gemfile
# Gemfile
# Update Rails
gem 'rails', '~> 5.0.0'
# Rails 5 requires these versions
gem 'rake', '>= 11.1'
# Update common gems
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
# If using MySQL
gem 'mysql2', '>= 0.3.18', '< 0.5'
# If using PostgreSQL
gem 'pg', '~> 0.18'
# ActiveJob adapter (if not using default)
gem 'sidekiq', '~> 4.2' # or preferred adapterbundle update rails
bundle installStep 2: Run the Update Task
Rails provides an update task to generate new configuration files:
rails app:updateThis will prompt to overwrite or merge configuration files. Review each change carefully before accepting.
Key files to review:
config/application.rbconfig/environments/*.rbconfig/initializers/new_framework_defaults.rb(new in Rails 5)
Step 3: Critical Breaking Changes
1. belongs_to Now Required by Default
In Rails 5, belongs_to associations are required by default.
Before (Rails 4.2):
class Comment < ApplicationRecord
belongs_to :post
end
# This was valid
Comment.create(body: "Great post!")After (Rails 5):
class Comment < ApplicationRecord
belongs_to :post
end
# This now raises ActiveRecord::RecordInvalid
Comment.create(body: "Great post!")
# => ActiveRecord::RecordInvalid: Validation failed: Post must exist
# Must provide post
Comment.create(body: "Great post!", post: post)
# Or make it optional
class Comment < ApplicationRecord
belongs_to :post, optional: true
end-- Rails 5 validates before attempting INSERT
-- If post_id is nil, validation fails before this SQL runs:
INSERT INTO "comments" ("body", "post_id", "created_at", "updated_at")
VALUES ('Great post!', NULL, '2025-12-05 10:30:00', '2025-12-05 10:30:00')
-- With valid post_id, SQL executes normally:
INSERT INTO "comments" ("body", "post_id", "created_at", "updated_at")
VALUES ('Great post!', 1, '2025-12-05 10:30:00', '2025-12-05 10:30:00')Migration Strategy:
Search for all belongs_to associations and add optional: true where appropriate:
# Find all belongs_to associations
grep -r "belongs_to" app/models/2. before_filter → before_action
Rails 5 deprecates before_filter, after_filter, and around_filter.
Before:
class ApplicationController < ActionController::Base
before_filter :authenticate_user!
after_filter :log_action
endAfter:
class ApplicationController < ActionController::Base
before_action :authenticate_user!
after_action :log_action
endQuick Fix:
# Find and replace across all controllers
find app/controllers -type f -exec sed -i 's/before_filter/before_action/g' {} +
find app/controllers -type f -exec sed -i 's/after_filter/after_action/g' {} +
find app/controllers -type f -exec sed -i 's/around_filter/around_action/g' {} +3. render :text Deprecated
Before:
def show
render text: "Hello World"
endAfter:
def show
render plain: "Hello World"
# or
render html: "<h1>Hello World</h1>".html_safe
end4. ActiveRecord Callbacks
Returning false in a callback no longer halts the callback chain.
Use throw :abort instead.
Before:
class User < ApplicationRecord
before_save :check_admin
def check_admin
return false if admin_changed? && !current_user.super_admin?
end
endAfter:
class User < ApplicationRecord
before_save :check_admin
def check_admin
throw :abort if admin_changed? && !current_user.super_admin?
end
end5. ActiveRecord Relation Changes
Rails 5 improves ActiveRecord querying with better consistency and introduces the or method for complex queries.
New or method for combining conditions:
# Rails 5 introduces .or method
User.where(admin: true).or(User.where(moderator: true))-- Generated SQL
SELECT "users".* FROM "users"
WHERE ("users"."admin" = 1 OR "users"."moderator" = 1)where.not behavior improvements:
# Single value exclusion (same as Rails 4.2)
User.where.not(role: nil)
# Multiple value exclusion
User.where.not(role: ['admin', 'moderator'])
# Complex combinations with or
User.where(active: true).or(User.where.not(role: 'guest'))-- Single value exclusion SQL
SELECT "users".* FROM "users" WHERE "users"."role" IS NOT NULL
-- Multiple value exclusion SQL
SELECT "users".* FROM "users" WHERE "users"."role" NOT IN ('admin', 'moderator')
-- Complex combination SQL
SELECT "users".* FROM "users"
WHERE ("users"."active" = 1 OR "users"."role" != 'guest')Key improvements:
- New
ormethod: Chain multiple conditions with OR logic instead of raw SQL - Better error handling: More consistent behavior across different query types
- Improved SQL generation: More efficient queries in complex scenarios
- Structural compatibility:
ormethod validates that relations can be combined
Note: The
ormethod requires both relations to be structurally compatible (same table, similar structure). For more advanced querying patterns, see our post on Rails 7 ActiveRecord query improvements.
Step 4: Migration Versioning
Rails 5 introduces migration versioning to maintain compatibility.
New migration format:
# Rails 4.2
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.timestamps null: false
end
end
end
# Rails 5
class CreateUsers < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
t.string :name
t.timestamps
end
end
endUpdate existing migrations:
# Find all migrations
find db/migrate -type f -name "*.rb"
# Add version to each migration class
# Change: class CreateUsers < ActiveRecord::Migration
# To: class CreateUsers < ActiveRecord::Migration[4.2]Step 5: Integer Column Changes
Rails 5 changes default integer column limits.
Before (Rails 4.2):
integer→ 4 bytes (limit: 4)
After (Rails 5):
integer→ 4 bytes by defaultbigint→ 8 bytes (for large numbers)
For primary keys:
Note: Doesn’t apply to already migrated tables
# Rails 5 uses bigint for primary keys by default
create_table :users do |t|
# id is automatically bigint
t.string :name
end
# To use regular integer (not recommended)
create_table :users, id: :integer do |t|
t.string :name
endStep 6: ActionController Changes
Strong Parameters
Strong parameters are now required for all mass assignment.
Before:
def create
@user = User.create(params[:user])
endAfter:
def create
@user = User.create(user_params)
end
private
def user_params
params.require(:user).permit(:name, :email, :role)
endCSRF Protection
Rails 5 uses protect_from_forgery with: :exception by default.
# config/application.rb or ApplicationController
class ApplicationController < ActionController::Base
# Rails 5 default
protect_from_forgery with: :exception
# If we need the old behavior
# protect_from_forgery with: :null_session
endStep 7: Asset Pipeline Updates
Sprockets 3
Rails 5 uses Sprockets 3, which has some changes:
# Gemfile
gem 'sprockets', '~> 3.7'
gem 'sprockets-rails', '~> 3.2'Manifest Files
Update manifest files if needed:
// app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require_tree .Step 8: Testing Updates
Controller Tests
Controller test methods have changed to be more explicit about parameters:
Before (Rails 4.2):
get :show, id: 1
post :create, user: { name: "John" }After (Rails 5):
get :show, params: { id: 1 }
post :create, params: { user: { name: "John" } }-- Both generate the same SQL queries:
-- GET request triggers:
SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
-- POST request triggers (with strong parameters):
INSERT INTO "users" ("name", "created_at", "updated_at")
VALUES ('John', '2025-12-05 10:30:00', '2025-12-05 10:30:00')Why the change?: Rails 5 makes parameter passing more explicit to improve test clarity and prevent confusion between different types of test arguments.
Test Helper Updates
# test/test_helper.rb
# Rails 5 uses fixtures differently
fixtures :all
# Update assertions if needed
assert_difference 'User.count', 1 do
post users_path, params: { user: { name: "John" } }
endStep 9: Common Gotchas
1. Autoloading in Production
Rails 5 disables autoloading in production by default.
# config/environments/production.rb
# This is now false by default
config.eager_load = true2. Halting Callbacks
Remember to use throw :abort instead of returning false.
3. JSON Serialization
Rails 5 changes how ActiveRecord serializes to JSON. The key change: timestamps are now serialized as ISO 8601 strings instead of the previous format.
Before (Rails 4.2):
user = User.create(name: "John", email: "[email protected]")
user.to_json
# => {"id":1,"name":"John","email":"[email protected]","created_at":"2025-12-05 10:30:00 UTC"}After (Rails 5):
user = User.create(name: "John", email: "[email protected]")
user.to_json
# => {"id":1,"name":"John","email":"[email protected]","created_at":"2025-12-05T10:30:00.000Z"}-- Database storage remains the same in both versions:
SELECT id, name, email, created_at FROM users WHERE id = 1;
-- => 1 | John | [email protected] | 2025-12-05 10:30:00.000000
-- The difference is only in JSON serialization format:
-- Rails 4.2: "2025-12-05 10:30:00 UTC"
-- Rails 5: "2025-12-05T10:30:00.000Z" (ISO 8601)If we have custom serialization, review and test it:
class User < ApplicationRecord
def as_json(options = {})
super(options.merge(only: [:id, :name, :email]))
end
endImpact: If our API clients parse timestamps, they may need updates. Test API responses thoroughly.
4. Zeitwerk Preparation
While Zeitwerk comes in Rails 6, start preparing:
- Follow naming conventions strictly
- Avoid circular dependencies
- Use proper file structure
Step 10: Run the Tests
# Run full test suite
rails test
# Or with RSpec
bundle exec rspec
# Check for deprecation warnings
RAILS_ENV=test rails test 2>&1 | grep -i deprecationFix any failing tests before deploying.
Step 11: Update Staging/Production
Deployment Checklist
- Update Ruby version on servers
- Run
bundle installon production - Run
rails db:migrate - Precompile assets:
RAILS_ENV=production rails assets:precompile - Restart application servers
- Monitor error logs
- Check performance metrics
Rollback Plan
# If issues arise, rollback:
git checkout previous-version
bundle install
rails db:rollback STEP=X
rails assets:precompile
# Restart serversPerformance Improvements
After upgrading, we should see:
- 10-15% faster request processing (Ruby 2.4 + Rails 5)
- Better memory usage with improved garbage collection
- Faster asset compilation with Sprockets 3
- ActionCable for real time features without external dependencies
Upgrade Checklist
Note: This checklist covers the most common changes. Depending on the application’s gems, custom code, and architecture, we may encounter additional issues. Always test thoroughly in a staging environment.
- Upgrade Ruby to 2.3+ or 2.4
- Update Gemfile with Rails 5.0
- Run
rails app:update - Add
optional: trueto belongs_to where needed - Replace
before_filterwithbefore_action - Update
render :texttorender plain: - Fix callbacks to use
throw :abort - Add migration versions
[4.2]or[5.0] - Update controller tests to use
params:keyword - Review third-party gem compatibility
- Check custom serializers and API responses
- Run full test suite
- Test in staging environment
- Deploy to production with monitoring
What’s Next
We’ve successfully upgraded from Rails 4.2 to Rails 5. In the next post, we’ll tackle Rails 5.2 to Rails 6, which introduces the biggest change: Zeitwerk autoloader.
Coming up:
- Part 3: Rails 5.2 to Rails 6 - Zeitwerk, Webpacker, multiple databases, and Ruby 2.5+
- Part 4: Rails 6.1 to Rails 7 - Import maps, Hotwire, Ruby 3
- Part 5: Rails 7.2 to Rails 8 - Solid Queue, authentication, Ruby 3.3
Resources
At Saeloun, we’ve helped dozens of teams successfully upgrade from Rails 4.2 to modern versions. If facing challenges with the upgrade or need expert guidance, we’re here to help.
