Thread-Safety in Ruby on Rails: Basic Example With Race Conditions

The Global Interpreter Lock (GIL) in Ruby prevents true multi-core CPU usage in a Rails app, but there is still concurrency, especially when using a multi-threaded server like Puma. While Ruby cannot execute multiple threads in parallel due to the GIL, Puma can handle multiple requests concurrently. This means that when one thread is waiting for a database transaction or an external service, other threads can continue working.

Imagine we have a simple controller that increments the page view counter. If 100 users hit the same page within one second, we could face a problem. Without using atomic operations or a Mutex, some of those page views could be lost, and the counter might not reflect the correct total. This happens due to a race condition, where multiple threads are trying to update the same resource at the same time.

# BAD PRACTICE

class PageViewsController < ApplicationController
  def show
    @page_view = PageView.find(params[:id])
    @page_view.view_count += 1
    @page_view.save
    
    render json: @page_view
  end 
end

If multiple users request this page at the same time, say 5 users hit the show action concurrently, the following could happen:

  • Each user (request) fetches the same PageView record from the database.
  • Each request reads the current view_count (e.g., 100).
  • Each request increments the view_count to 101.
  • Each request saves view_count as 101 in the database.

Even though 5 requests were made, the view_count was only incremented by 1 instead of 5 because the changes made by one request are overwritten by others. This is a classic race condition due to concurrent database access in a multi-threaded environment (handled by Puma)

# GOOD PRACTICE

class PageViewsController < ApplicationController
  def show
    @page_view = PageView.find(params[:id])
    @page_view.increment!(:view_count)

    render json: @page_view
  end 
end

The increment! method generates an SQL query that increments the view_count directly in the database in an atomic way. Even if multiple requests hit the server at the same time, the database ensures that each increment operation is applied correctly, preventing lost updates

Author

Need to improve your Rails app? Let’s talk.


SCHEDULE A CALL TODAY

Leave a Reply

Your email address will not be published. Required fields are marked *