---
title: "Recyclable cache keys in Rails"
description:
  "Rails added cache_versioning and collection_cache_versioning to support
  recyclable cache keys."
canonical_url: "https://www.bigbinary.com/blog/rails-adds-support-for-recyclable-cache-keys"
markdown_url: "https://www.bigbinary.com/blog/rails-adds-support-for-recyclable-cache-keys.md"
---

# Recyclable cache keys in Rails

Rails added cache_versioning and collection_cache_versioning to support
recyclable cache keys.

- Author: Taha Husain
- Published: August 6, 2019
- Categories: Rails 5.2, Rails 6, Rails

[Recyclable cache keys](https://github.com/rails/rails/pull/29092) or _cache
versioning_ was introduced in Rails 5.2. Large applications frequently need to
invalidate their cache because cache store has limited memory. We can optimize
cache storage and minimize cache miss using recyclable cache keys.

Recyclable cache keys is supported by all
[cache stores](https://guides.rubyonrails.org/caching_with_rails.html#cache-stores)
that ship with Rails.

Before Rails 5.2, `cache_key`'s format was _{model_name}/{id}-{update_at}_. Here
`model_name` and `id` are always constant for an object and `updated_at` changes
on every update.

#### Rails 5.1

```ruby
>> post = Post.last

>> post.cache_key
=> "posts/1-20190522104553296111"

# Update post
>> post.touch

>> post.cache_key
=> "posts/1-20190525102103422069" # cache_key changed

```

In Rails 5.2, `#cache_key` returns _{model_name}/{id}_ and new method
`#cache_version` returns _{updated_at}_.

#### Rails 5.2

```ruby

>> ActiveRecord::Base.cache_versioning = true

>> post = Post.last

>> post.cache_key
=> "posts/1"

>> post.cache_version
=> "20190522070715422750"

>> post.cache_key_with_version
=> "posts/1-20190522070715422750"

```

Let's update `post` instance and check `cache_key` and `cache_version`'s
behaviour.

```ruby

>> post.touch

>> post.cache_key
=> "posts/1" # cache_key remains same

>> post.cache_version
=> "20190527062249879829" # cache_version changed

```

To use cache versioning feature, we have to enable
`ActiveRecord::Base.cache_versioning` configuration. By default
`cache_versioning` config is set to false for backward compatibility.

We can enable cache versioning configuration globally as shown below.

```ruby

ActiveRecord::Base.cache_versioning = true
# or
config.active_record.cache_versioning = true

```

Cache versioning config can be applied at model level.

```ruby

class Post < ActiveRecord::Base
  self.cache_versioning = true
end

# Or, when setting `#cache_versioning` outside the model -

Post.cache_versioning = true

```

Let's understand the problem step by step with cache keys before Rails 5.2.

#### Rails 5.1 (without cache versioning)

_1. Write `post` instance to cache using
[`fetch`](https://apidock.com/rails/ActiveSupport/Cache/Store/fetch) api._

```ruby

>> before_update_cache_key = post.cache_key
=> "posts/1-20190527062249879829"

>> Rails.cache.fetch(before_update_cache_key) { post }
=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 06:22:49">

```

_2. Update `post` instance using
[`touch`](https://apidock.com/rails/ActiveRecord/Persistence/touch)._

```ruby

>> post.touch
   (0.1ms)  begin transaction
  Post Update (1.6ms)  UPDATE "posts" SET "updated_at" = ? WHERE "posts"."id" = ?  [["updated_at", "2019-05-27 08:01:52.975653"], ["id", 1]]
   (1.2ms)  commit transaction
=> true

```

_3. Verify stale `cache_key` in cache store._

```ruby

>> Rails.cache.fetch(before_update_cache_key)
=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 06:22:49">

```

_4. Write updated `post` instance to cache using new `cache_key`._

```ruby
>> after_update_cache_key = post.cache_key
=> "posts/1-20190527080152975653"

>> Rails.cache.fetch(after_update_cache_key) { post }
=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 08:01:52">

```

_5. Cache store now has two copies of `post` instance._

```ruby

>> Rails.cache.fetch(before_update_cache_key)
=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 06:22:49">

>> Rails.cache.fetch(after_update_cache_key)
=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 08:01:52">

```

_cache_key_ and its associated instance becomes irrelevant as soon as an
instance is updated. But it stays in cache store until it is manually
invalidated.

This sometimes result in overflowing cache store with stale keys and data. In
applications that extensively use cache store, a huge chunk of cache store gets
filled with stale data frequently.

Now let's take a look at the same example. This time with _cache versioning_ to
understand how recyclable cache keys help optimize cache storage.

#### Rails 5.2 (cache versioning)

_1. Write `post` instance to cache store with `version` option._

```ruby

>> ActiveRecord::Base.cache_versioning = true

>> post = Post.last

>> cache_key = post.cache_key
=> "posts/1"

>> before_update_cache_version = post.cache_version
=> "20190527080152975653"

>> Rails.cache.fetch(cache_key, version: before_update_cache_version) { post }
=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 08:01:52">

```

_2. Update `post` instance._

```ruby

>> post.touch
   (0.1ms)  begin transaction
  Post Update (0.4ms)  UPDATE "posts" SET "updated_at" = ? WHERE "posts"."id" = ?  [["updated_at", "2019-05-27 09:09:15.651029"], ["id", 1]]
   (0.7ms)  commit transaction
=> true

```

_3. Verify stale `cache_version` in cache store._

```ruby

>> Rails.cache.fetch(cache_key, version: before_update_cache_version)
=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 08:01:52">

```

_4. Write updated `post` instance to cache._

```ruby

>> after_update_cache_version = post.cache_version
=> "20190527090915651029"

>> Rails.cache.fetch(cache_key, version: after_update_cache_version) { post }
=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 09:09:15">

```

_5. Cache store has replaced old copy of `post` with new version automatically._

```ruby

>> Rails.cache.fetch(cache_key, version: before_update_cache_version)
=> nil

>> Rails.cache.fetch(cache_key, version: after_update_cache_version)
=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 09:09:15">

```

Above example shows how recyclable cache keys maintains single, latest copy of
an instance. Stale versions are removed automatically when new version is added
to cache store.

_Rails 6_ added `#cache_versioning` for `ActiveRecord::Relation`.

`ActiveRecord::Base.collection_cache_versioning` configuration should be enabled
to use cache versioning feature on collections. It is set to false by default.

We can enable this configuration as shown below.

```ruby

ActiveRecord::Base.collection_cache_versioning = true
# or
config.active_record.collection_cache_versioning = true

```

Before Rails 6, `ActiveRecord::Relation` had `cache_key` in format
`{table_name}/query-{query-hash}-{count}-{max(updated_at)}`.

In Rails 6, cache_key is split in stable part `cache_key` -
`{table_name}/query-{query-hash}` and volatile part `cache_version` -
`{count}-{max(updated_at)}`.

For more information, check out
[blog on ActiveRecord::Relation#cache_key in Rails 5](https://blog.bigbinary.com/2016/02/02/activerecord-relation-cache-key.html).

#### Rails 5.2

```ruby

>> posts = Post.all

>> posts.cache_key
=> "posts/query-00644b6a00f2ed4b925407d06501c8fb-3-20190522172326885804"

```

#### Rails 6

```ruby

>> ActiveRecord::Base.collection_cache_versioning = true

>> posts = Post.all

>> posts.cache_key
=> "posts/query-00644b6a00f2ed4b925407d06501c8fb"

>> posts.cache_version
=> "3-20190522172326885804"

```

Cache versioning works similarly for `ActiveRecord::Relation` as
`ActiveRecord::Base`.

In case of `ActiveRecord::Relation`, if number of records change and/or
record(s) are updated, then same `cache_key` is written to cache store with new
`cache_version` and updated records.

## Conclusion

Previously, cache invalidation had to be done manually either by deleting cache
or setting cache expire duration. Cache versioning invalidates stale data
automatically and keeps latest copy of data, saving on storage and performance
drastically.

Check out the [pull request](https://github.com/rails/rails/pull/29092) and
[commit](https://github.com/rails/rails/commit/4f2ac80d4cdb01c4d3c1765637bed76cc91c1e35)
for more details.

## Links

- [Human page](https://www.bigbinary.com/blog/rails-adds-support-for-recyclable-cache-keys)
