All lessons

Spot the Tax · Card 15 of 20

update_all is a SQL escape hatch

Why a fast bulk update can quietly leave 50,000 users with broken state.

The code

What will this cost you in six months?

class User < ApplicationRecord
  before_save :extend_trial_if_needed

  private

  def extend_trial_if_needed
    self.trial_ends_at = 14.days.from_now if role == "trial"
  end
end

# In a rake task:
User.where(role: "free").update_all(role: "trial")

The problem

update_all sends a single SQL UPDATE statement directly. It skips callbacks, validations, and even updated_at. That's the whole point of it — it's the fast path. But it also means the before_save that sets trial_ends_at doesn't fire. 50,000 users now have their role set to "trial" but a nil trial_ends_at, and three days later support starts getting tickets from people whose trial expired the moment they signed up.

Take a moment. Before revealing, think about what you'd use instead when callbacks need to fire — and how you'd make it obvious in the code when callbacks are deliberately being skipped.