Upgrading from Rails 4.2 to Rails 5 - A Complete Guide

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
  • dig method 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.10

Important: 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 adapter
bundle update rails
bundle install

Step 2: Run the Update Task

Rails provides an update task to generate new configuration files:

rails app:update

This will prompt to overwrite or merge configuration files. Review each change carefully before accepting.

Key files to review:

  • config/application.rb
  • config/environments/*.rb
  • config/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_filterbefore_action

Rails 5 deprecates before_filter, after_filter, and around_filter.

Before:

class ApplicationController < ActionController::Base
  before_filter :authenticate_user!
  after_filter :log_action
end

After:

class ApplicationController < ActionController::Base
  before_action :authenticate_user!
  after_action :log_action
end

Quick 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"
end

After:

def show
  render plain: "Hello World"
  # or

  render html: "<h1>Hello World</h1>".html_safe
end

4. 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
end

After:

class User < ApplicationRecord
  before_save :check_admin

  def check_admin
    throw :abort if admin_changed? && !current_user.super_admin?
  end
end

5. 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 or method: 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: or method validates that relations can be combined

Note: The or method 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
end

Update 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 default
  • bigint → 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
end

Step 6: ActionController Changes

Strong Parameters

Strong parameters are now required for all mass assignment.

Before:

def create
  @user = User.create(params[:user])
end

After:

def create
  @user = User.create(user_params)
end

private

def user_params
  params.require(:user).permit(:name, :email, :role)
end

CSRF 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

end

Step 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" } }
end

Step 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 = true

2. 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
end

Impact: 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 deprecation

Fix any failing tests before deploying.

Step 11: Update Staging/Production

Deployment Checklist

  • Update Ruby version on servers
  • Run bundle install on 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 servers

Performance 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: true to belongs_to where needed
  • Replace before_filter with before_action
  • Update render :text to render 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.

Contact us for Rails upgrade consulting

Need help on your Ruby on Rails or React project?

Join Our Newsletter