Spot the Tax · Card 3 of 20
:destroy runs in Ruby, :delete_all runs in SQL
Why deleting one user with 50,000 events can take minutes, and how to pick the right cascade for the work.
The code
What will this cost you in six months?
class User < ApplicationRecord
has_many :events, dependent: :destroy
has_many :messages, dependent: :destroy
has_many :reactions, dependent: :destroy
end
# In the controller:
def destroy
current_user.destroy
redirect_to root_path
end The problem
When you delete a user that has 50,000 events, dependent: :destroy doesn't just delete the events. It loads each event into memory as a Ruby object, fires every destroy callback on that object, and then sends a separate DELETE statement for that one row. So a single user.destroy ends up doing 50,000 of everything. What should be a fast operation takes minutes, sometimes times out halfway through, and leaves your data in a half-deleted state that's painful to clean up.
Take a moment. Before revealing, think about how you'd actually delete a user with 50,000 events. What's the difference between deleting in Ruby and deleting in SQL, and when does each make sense?
The solution
Choose the cascade that matches the work you actually need to do. :delete_all sends a single DELETE statement to the database without ever loading the rows into Ruby, which is what you want most of the time. Use :destroy only when you genuinely need each child's destroy callbacks to fire — and when you do, run the deletion in a background job so a slow user doesn't take down your controller.
- Deletes happen at SQL speed
- Memory stays bounded regardless of how many associated rows exist
- For real
:destroycases, the work can move to a job that you can monitor and retry
class User < ApplicationRecord
has_many :events, dependent: :delete_all
has_many :messages, dependent: :delete_all
has_many :reactions, dependent: :delete_all
end The principle at play — Bulk operations in SQL
SQL and Ruby operate at very different layers, and the difference becomes a big deal as soon as you're working with many rows at once. SQL runs inside the database, in a single round trip, with the query planner figuring out the most efficient way to do the work. Ruby runs inside your application, one object at a time, and each object carries the cost of being loaded, instantiated, and pushed through whatever callbacks are defined on it.
When you write dependent: :destroy, you're choosing the Ruby version. Rails loads every associated record, instantiates each one, fires its destroy callbacks, and only then runs the DELETE for that single row. So 50,000 records means 50,000 objects in memory and 50,000 separate DELETE statements. dependent: :delete_all is the SQL version: one statement, done in milliseconds, no Ruby objects involved.
They look like two flavors of the same operation, but they're really two completely different operations that happen to share a similar name. The decision between them comes down to whether you actually need callbacks on the children. Most of the time, the honest answer is that you don't, and the SQL version is what you want.