Rails 7 allows setting cache expiry, as an absolute timestamp


Rails caching has always had an expires_in method, which allows us to set cache expiry in relative time.

Before : Caching Expiry with expires_in

Let’s say in our e-commerce Rails application we are running a sale that ends in 2 days. We store the product ids which are part of sale in cache as below:

product_ids = ["4c383b29-c2d4-4579-be56-bb5fa51b43b5", "8ed1274b-e0ca-44f8-9ef2-9892c1816ba1", ...]

Rails.cache.write("new_year_sale_products", product_ids, expires_in: 2.days)
# Time passed to expires_in will be converted to epochs
# 2.days is stored as 172800 seconds

Rails.cache.send(:read_entry, "new_year_sale_products").instance_variable_get(:@expires_in)
# => 172800.0

Rails.cache.read("new_year_sale_products")
# => ["4c383b29-c2d4-4579-be56-bb5fa51b43b5", "8ed1274b-e0ca-44f8-9ef2-9892c1816ba1", ...]
# returns nil when called after 2 days.

The expires_in is not an absolute timestamp.

After : Caching Expiry with expires_at

With this merge, it is now possible to store cache expiration in absolute time.

As mentioned in the PR itself, there are several use cases like:

  • To expire an API token with a precise expiry time
  • Cache until a precise cutoff time

To end the sale after 2 days we can make the below changes to our previous example.

product_ids = ["4c383b29-c2d4-4579-be56-bb5fa51b43b5", "8ed1274b-e0ca-44f8-9ef2-9892c1816ba1", ...]

Rails.cache.write("new_year_sale_products", product_ids, expires_at: (Time.now + 2.days).end_of_day)
# (Time.now + 2.days).end_of_day will return 2021-07-30 23:59:59.999999999

Rails.cache.send(:read_entry, 'new_year_sale_products', {})&.expires_at
# Returns 1627669800.0 ( = Time.parse("2021-07-30 23:59:60").to_i)

We need to be careful when passing values to the expires_at and expires_in keys.

# What happens if you pass relative time to expires_at

Rails.cache.write("new_year_sale_products", product_ids, expires_at: 5.minutes)
# 5.minutes will set expires_at to 300 ( Thursday, 1 January 1970 00:05:00 )

Rails.cache.read("new_year_sale_products")
# nil

Differences: expires_in vs expires_at

  • The biggest difference is storing time in Relative vs Absolute epochs.
  • expires_in can be set in the constructor, expires_at is set at individual writes.
      config.cache_store = :redis_cache_store, { url: "redis://localhost:6379", expires_in: 1.day }
      # For all keys the default expires_in is 1.day
    
      # We can override default expires_in as below
      Rails.cache.write "key", "value", expires_in: 3.hours
    
  • If both the expires_in and expires_at are set, expires_at gets priority.