All lessons

Spot the Tax · Card 11 of 20

Read-modify-write isn't atomic

Why decrementing a counter in Ruby quietly miscounts the moment two requests arrive at once.

The code

What will this cost you in six months?

def consume_credit
  if current_user.credits > 0
    current_user.update(credits: current_user.credits - 1)
    perform_action
  else
    render :insufficient_credits
  end
end

The problem

When the user has two browser tabs open or accidentally double-clicks the action, two requests arrive at almost the same time. Both of them read the user's credits at, say, 5. Both compute 5 - 1 = 4. Both write 4 back to the database. The user just performed two actions but only paid for one. You won't reproduce this in development, but a user with bad intentions will figure it out the moment your service has anything worth gaming.

Take a moment. Before revealing, think about how you'd make sure a credit can only be spent once, even when two requests run at the same instant. Where does the check have to live?