Compressing Cache in Rails

Caching is an essential technique used by web applications to improve their performance. Rails, being a popular web framework, has an in-built caching system that allows developers to store the results of expensive computations or database queries in memory or on disk. Caching can significantly reduce the response time of a web application, resulting in a better user experience.

However, as the amount of cached data grows, so does the memory and disk space used by the cache. This can lead to performance issues and increased hosting costs. In this blog, we’ll discuss how to compress cache in Rails to save space and improve performance.

Compressing Cache in Rails

Rails provides built-in support for compressing cache data. By default, Rails uses the Marshal module to serialize and deserialize cache data. However, this can be inefficient and result in large cache files. To compress cache data, Rails provides the :compress option, which uses the Zlib module to compress and decompress cache data.

To enable compression for a cache store, you can add the :compress option to your cache configuration file. For example, to enable compression for the file_store cache store, you can modify the config/environments/production.rb file as follows:

config.cache_store = :file_store, "tmp/cache/", { compress: true }

# for redis
# Use a different cache store in production.
config.cache_store = :redis_cache_store, {url:                ENV.fetch('REDIS_URL'),
                                            compress_threshold: 1 * 1024, # 1.byte
                                            compress:           true,
                                            expires_in:         3.months}

Compression Threshold

Compression can be costly and hamper performance if you are compressing smaller data. So, in rails default threshold is 1KB. Data larger than this, is compressed by default.

With this configuration, Rails will compress cache data before storing it on disk and decompress it when retrieving it. This can significantly reduce the size of cache files and improve the performance of your web application.

Using .fetch with a Block

Rails provides the .fetch method to retrieve data from the cache store. The .fetch method accepts a key and an optional set of options. If the key exists in the cache store, .fetch returns the cached data. Otherwise, it yields to a block, and the result of the block is stored in the cache store under the specified key.

The advantage of using .fetch with a block is that it ensures that the cache key is set, even if the value is not present in the cache. This can prevent expensive computations or database queries from being performed unnecessarily. For example, suppose you have a method that performs an expensive calculation to determine the current time. In that case, you can use .fetch to store the result of the calculation in the cache and retrieve it on subsequent calls.

Here’s an example of how to use .fetch with a block:

time = Rails.cache.fetch("current_time") do
  Time.zone.now
end

In this example, if the "current_time" key exists in the cache store, the cached value is returned. Otherwise, the block is executed, and the result of Time.zone.now is stored in the cache store under the "current_time" key.

Using force: true with .fetch

Sometimes, you may want to force the cache store to retrieve the value from the block even if the key exists in the cache store. For example, suppose you have updated the underlying data and want to refresh the cached value. In that case, you can pass the force: true option to the .fetch method. This option tells the cache store to retrieve the value from the block and store it in the cache store, even if the key exists.

Here’s an example of how to use force: true with .fetch:

time = Rails.cache.fetch("current_time", force: true) do
  Time.zone.now
end

In this example, the "current_time" key is retrieved from the cache store, and the block is executed to calculate the current time. The result of the block is then stored in the cache store under the current_time.

Using .write

In addition to the .fetch method, Rails also provides the .write method to store data in the cache store directly. The .write method accepts a key, a value, and an optional set of options. The value can be any object that can be serialized by the cache store.

Here’s an example of how to use .write:

Rails.cache.write("my_key", "my_value")

In this example, the string "my_value" is stored in the cache store under the key "my_key".

The .write method can also take an optional :expires_in option, which specifies the time-to-live (TTL) for the cached data. The :expires_in option is specified in seconds and can be a positive integer or a ActiveSupport::Duration object. After the TTL expires, the cached data is considered stale and is removed from the cache store.

Here’s an example of how to use .write with an :expires_in option:

Rails.cache.write("my_key", "my_value", expires_in: 1.hour)

In this example, the string "my_value" is stored in the cache store under the key "my_key". The cached data will expire and be removed from the cache store in one hour.

Note that the .write method overwrites any existing value in the cache store for the specified key. If you want to update the value of an existing key without overwriting it, you should use the .fetch method instead.

Enabling/Disabling Compression on Usage

Despite of compress: true in root config you can choose not to compress using compress: false option.

Rails.cache.write("my_key", "my_value", expires_in: 1.hour, compress: false)

time = Rails.cache.fetch("current_time", force: true, compress: false) do
  Time.zone.now
end

How to check if Rails is compressing cache-data?

Note: The cache value needs to be greater than default cache-threshold (1KB), so we are multiplying it with 1000.

# Rails 7
(dev) > Rails.cache.fetch('foo', compress: true, force: true){'val'*1000}.truncate(100)
=> "valvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalvalv..."

# it stores the cache.
# now, reading directly from the store
(dev) > Rails.cache.redis.get('foo').truncate(100)
=> "\u0004\bo: ActiveSupport::Cache::Entry\n:\v@value\"0x\x9Cc\xE1\xF0Tb\xDA\xC1]\x96\x983\x8AF\xD1(\u001AE\xA3h\u0014\x8D\xA2Q4\x8AF\xD1(\u001A\xE4\x88͊\xCD5\u0004\u0000g \xF0\u0010:\r@version0:..."

# we see value is garbled that makes sure its compressed.

Summary

In conclusion, caching can significantly improve the performance of your Rails application, and compressing the cache can save disk space and improve performance further. The .fetch and .write methods are essential tools for interacting with the cache store in Rails, and using them effectively can help you build fast and efficient web applications.