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.