There are different reasons why a ActiveRecord::StaleObjectError would occur. Sometimes it is caused because rails does not have a stable identity map yet. This means that if the same object would be retrieved via different associations, your rails process would not know they point to the same object, would keep different copies of the same object and that could easily cause a StaleObjectError if you attempt to change both.

Such an occurrence needs to be fixed structurally in your code.

But sometimes you can get StaleObjectError because things happen at the same time. There are two ways to handle this:

  • make sure that controller actions which affect the same object, are always executed sequentially. One way to achieve this is to lock the object you want to update in the database. Then all other processes wanting to update the same object will have to wait until the lock is released (and the lock can be acquired). This is a valid approach but costly, intrusive (you need to explicitly add code), and possibly dangerous. It is normally easy to avoid, but you have to be careful for deadlocks and locks that are never released.
  • when a StaleObjectError occurs, just retry the request

Now that second option seems valid, if only it would be easy to handle. Luckily there is a very easy and unobtrusive way to automatically retry all requests when a StaleObjectError occurs. Let’s just create a middleware that catches the exception and retries the complete request a few times.

Create a new file lib/middleware/handle_stale_object_error.rb containing the following:

module Middleware

  class HandleStaleObjectError
    RETRY_LIMIT = 3

    def initialize(app)
      @app = app
    end

    def call(env)
      retries = 0

      begin
        @app.call(env)
      rescue ActiveRecord::StaleObjectError
        raise if retries >= RETRY_LIMIT

        retries += 1
        Rails.logger.warn("HandleStaleObjectError::automatically retrying after StaleObjectError (attempt = #{retries})")

        retry
      end
    end
  end

end

Then in your config/application.rb you need to add the following lines at the bottom to activate the middleware (this requires a restart of your rails server):

    #
    # Middleware configuration
    #
    require 'middleware/handle_stale_object_error'

    config.middleware.insert_after ActiveRecord::SessionStore, Middleware::HandleStaleObjectError

Warning: this code will retry all occurences of StaleObjectError. For some occurrences this will not help at all, so you have to use your own judgement if this middleware is something you want in your codebase. I like this approach because it is an “optimistic” approach: it only adds extra processing when a StaleObjectError occurs, and when a not-fixable StaleObjectError it still fails as it should.

Hope this helps.