Faking Regex-Based Cache Keys in Rails

There are many ways to cache data in a Rails application. (The official Rails Guide explains the different approaches well.) Heroku allows you to easily take advantage of Rails caching by connecting your application to Memcached servers through the Dalli client gem.

This drop in solution is simple, but there's one glaring problem. Memcache does not support expiring cache keys with regex. This means if you need to delete keys beginning with "user-1" but not keys beginning with "user-2", you're out of luck. Or so it seems…

When it comes to deleting the cache, your options are to either empty the whole thing, like this:


…or delete a specific key, like this:


Not long ago, we worked on a data-heavy application that cached some dynamic endpoints with the user's id as a namespace. The keys looked like this:

user-1-foo user-2-foo

When a user hit the foo endpoint, we served up the cached JSON, specific to that user, with blazing fast Memcache speed. The problem arrived when we needed to expire the cache for a single user. Our first plan was to just use Rails' built-in regex expiry method:


Lo and behold, Memcache does not support delete_matched. When you think about it, this makes sense. Memcache's prowess is raw speed, which it attains by limiting interactions to simple key/value writes or deletes. Deleting keys by matching a regex pattern is more complex that those two operations, so Memcache chooses not to support it.

A common pattern to get around this limitation is to namespace keys with an integer:

user-1-memcache-iterator-4-foo user-2-memcache-iterator-4-foo

When you need to expire a given user's cache, simply increment the integer that is attached to that user. All queries going forward will use this new number in their keys. The keys that have the user's old memcache-iterator are essentially deleted, since they'll never again be queried. Now, if we want to flush the cache for just user 1, we'll bump up his memcache-iterator to 5 with this simple method:

class User
  def increment_memcache_iterator
    Rails.cache.write("user-#{}-memcache-iterator", self.memcache_iterator + 1)

  def memcache_iterator
    # fetch the user's memcache key
    # If there isn't one yet, assign it a random integer between 0 and 10
    Rails.cache.fetch("user-#{}-memcache-iterator") { rand(10) }

We use another method to build up the cache key for our example "foo" endpoint, ensuring that it always utilizes the users's current memcache-iterator:

class User
  def foo_cache_key

Finally, we tell Rails to use this foocachekey method when it needs to grab data from Memcache:

class FooController
  caches_action :foo, :cache_path => { |c| current_user.foo_cache_key }

This solution is handy because it allows you flush the cache for one user, but keep another user's cache intact. It's also blazing fast since all you're doing is incrementing a cached integer. As a bonus, the moment a user's memcache-iterator is changed, we queue up jobs to populate the cache with the new keys:

class User
  def eager_load_foo
    if Rails.cache.fetch(self.foo_cache_key).nil? # a cache of the current iterator doesn't exist, so create it
      Rails.cache.write(self.foo_cache_key, Foo.to_json)