Rails ActiveRecord Increment - Avoiding Race Conditions

Written by Ari Summer·
·Last updated July 16, 2023

ORMs like Rails’ ActiveRecord are great, but sometimes they make it so easy that we’re not thinking about what’s happening underneath, resulting in hard to track down bugs.

Race conditions are particularly hard to track down and reason about. If you Google for how to increment a column in Rails, you’ll see a lot of solutions using #increment and #increment!. If you look at the source for these methods, you’ll see:

activerecord/lib/active_record/base.rb, line 2679
def increment(attribute, by = 1)
  self[attribute] ||= 0
  self[attribute] += by
  self
end
activerecord/lib/active_record/base.rb, line 2689
def increment!(attribute, by = 1)
  increment(attribute, by).update_attribute(attribute, self[attribute])
end

What’s the problem? Well let’s say we are incrementing a click count whenever a user clicks on a page:

app/controllers/pages_controller.rb
class PagesController < ApplicationController
  def click
    @page = Page.find(params[:id])
    @page.increment!(:clicks, 1)
  end
end

Now let’s imagine two requests come in at the exact same time. Since #increment is incrementing the column in-memory, we have a race condition! Both requests could increment the column at the same time, in-memory, resulting in a count that only increments by one instead of two.

How do we avoid this race condition? We can push the concern of incrementing to the database. Rather than incrementing in memory, let’s have the database do it:

Page.where(id: params[:id]).update_all("click = click + 1")

This results in the following SQL:

UPDATE "pages" SET count = count + 1 WHERE "pages"."id" = $1;

Updates like this are atomic and concurrent requests won’t be a problem. In our example above, the click count would be incremented by two, just as it should!

Note that this is exactly what the lesser-known #update_counters method does. Next time you need to increment or decrement some database columns, I hope this tip comes in handy!