Practice · Scaling · Card 6
What's wrong with this counter increment under load?
Locally the test passes. In production the counter drifts off the real number over time. The code reads correct. The model is what fails.
The code
Tracking page views. Each visit calls this.
def increment_views(post_id)
post = Post.find(post_id)
post.update!(views_count: post.views_count + 1)
end The question
Two requests for the same post arrive at the same millisecond. The final counter ends up at 1, not 2. Why?
Take a moment. Pick the best answer. Wrong picks reveal why they're wrong, which is half the point.
✅ Answer breakdown
✗ A. ActiveRecord swallows the second update silently when the value doesn't change.
AR doesn't do that. Both updates would attempt to run; the issue is what they're writing.
✓ B. Read-modify-write race. Both requests read the same starting value, both write that value + 1.
Request A reads views_count = 10. Request B reads views_count = 10 before A writes. A writes 11. B writes 11. The net is +1, not +2. The fix: use an atomic increment, e.g. Post.where(id: id).update_all("views_count = views_count + 1") or post.increment!(:views_count).
✗ C. Postgres automatically locks the row when you call find.
Not unless you call .lock or find_by!(...).lock!. A bare find issues a regular SELECT with no lock.
✗ D. The counter column hit MAX_INT.
Wrong scale. We're going from 10 to 11, not approaching 2^31.
💡 The principle
Any time your code reads a value, computes a new value from it, and writes the new value back, you have a read-modify-write window. If two processes can be inside that window for the same row at once, one of the writes is lost.
Two common fixes:
- Atomic SQL.
update_all("views_count = views_count + 1")orincrement!let the database do the math. No window. - Optimistic locking. Add a
lock_versioncolumn; ActiveRecord raisesStaleObjectErrorif someone else updated the row since you read it. You retry.
The slow fix (don't reach for it first) is row-level pessimistic locking with .lock. It serializes everything that touches the row.
📚 Theory
For the full walkthrough, read Scaling · The Concurrency Model.