---
title: A thread-safety gotcha with CurrentAttributes
teaser: How memoization inside CurrentAttributes caused user data to leak between
  requests.
tags: rails,concurrency,parallelism
author: Justin Toniazzo
published_on: 2025-07-15
---

My team recently encountered a subtle, thread-safety bug in our Rails app which
stemmed from a misunderstanding of how
[`ActiveSupport::CurrentAttributes`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html)
works. It looked like memoization—caching an object for the duration of a
request—but it ended up leaking user data across requests.

## The bug

We had a method like this in `app/models/current.rb`

```ruby
class Current < ActiveSupport::CurrentAttributes
  attribute :user
  # ...
  private

  def feature_flags
    @_feature_flags ||= FeatureFlags.new(user)
  end
end
```

We memoized (or cached) this because instantiating `FeatureFlags.new` was a
little involved, and we wanted to do it just once per request. This resulted in
a bug where the feature flags for the first user to login would persist for all
subsequent users. It didn’t reset for each request like we expected.

After some digging, we realized that `CurrentAttributes` only resets the values
of explicitly defined `attribute`s between requests—not any other instance
variables you define. Thus to fix our issue, we converted `feature_flags` from
a method to an `attribute`.

```ruby
class Current < ActiveSupport::CurrentAttributes
  attribute :feature_flags
  attribute :user
end

# in some controller
Current.user = user
Current.feature_flags = FeatureFlags.new(user)
```

Now, `CurrentAttributes` knows to reset it around each request, and we have no
more thread safety bugs.

## Memoizing and singletons

We're used to using memoization in our controllers to cache expensive calls, so
we had assumed that memoization would work the same in `Current`. For instance
it’s not at all uncommon to see code like this

```ruby
class ResourcesController < ApplicationController
  # ...
  private

  def resource
    @_resource ||= Resource.find(params[:id])
  end
end
```

This works because Rails gives you a new instance of a controller for each
request, so any memoization you do will get wiped out after each request. There
are no thread safety issues with this form of memoization.

`CurrentAttributes` works differently. It’s implemented as a thread-local
singleton, meaning there's one shared instance per thread—not per request. The
first time it’s accessed, it will be instantiated, and then all subsequent
requests use the same instance. Singletons are a more common design pattern in
other languages, but don’t often make an appearance in Ruby.

`CurrentAttributes` handles this by [manually
resetting](https://github.com/rails/rails/blob/3235827585d87661942c91bc81f64f56d710f0b2/activesupport/lib/active_support/current_attributes.rb#L218-L222)
its attributes back to their default values around each request. Because we had
cached `feature_flags` as an instance variable rather than an `attribute`,
however, `CurrentAttributes` didn’t know it needed to reset it.

## Rails to the rescue

After investigating and fixing our issue, we were happy to find that a [Rails
PR to fix this issue](https://github.com/rails/rails/pull/55139) was recently
merged. If we had waited until it was released, our issue would have been fixed
without us having to do anything. Still, it was interesting to debug an issue
with a singleton, since they're pretty uncommon in Ruby.

## Caching is scary

Forms of caching, such as memoization, are frequently subject to weird bugs
like this. It’s very easy to introduce bugs that don’t occur locally, either
because caching is disabled completely in development, or because you don’t
have enough concurrent requests for a bug to appear.

It’s not a bad idea to [avoid caching until you’re sure you need
it](https://thoughtbot.com/blog/ruby-memoization-and-alternatives).

In cases where you do need caching, always consider the lifetime of the cached
object. If you’re memoizing in a controller, the lifetime of your cache is a
single request. But outside of that, it might not be what you expect.
