Using The Rails Cache With Redis

An entry-level introduction to setting up the Rails cache and seeing it in action with Redis in development.

On this page

This post will be a brief introduction to setting up the Rails cache and seeing it in action with Redis in development.

We will look at the configuration required from Rails but it will expect that you know a little about Redis and have it already installed and setup on your local machine.

Source code can be found here

Prerequisites

  1. Basic familiarity with setting up a new Rails project.
  2. basic familiarity with Redis.

Getting started

We will use Rails to initialize the project demo-rails-cache:

# Create a new rails project
$ rails new demo-rails-with-react-frontend
$ cd demo-rails-with-react-frontend

# Create a controller to demo the cache
$ bin/rails g controllers hello index

At this stage, we are ready to adjust some files and configuration for the demo.

Adjusting the application configuration

We are turning off the default forgery protection so that we can demonstrate the cache on the command-line.

require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module DemoRailsCache
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 7.0

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")
    config.action_controller.default_protect_from_forgery = false if ENV['RAILS_ENV'] == 'development'
  end
end

Adjusting the development configuration

Next is to update our development environment configuration to use Redis instead of the default memory store.

I am omitting a lot of the configuration that is already there, so ensure that you only adjust the following:

require 'active_support/core_ext/integer/time'

Rails.application.configure do

  # ... omitted

  if Rails.root.join('tmp/caching-dev.txt').exist?
    config.action_controller.perform_caching = true
    config.action_controller.enable_fragment_cache_logging = true

    # CHANGE HERE
    # config.cache_store = :memory_store
    config.cache_store = :redis_cache_store, { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/1') }
    config.public_file_server.headers = {
      'Cache-Control' => "public, max-age=#{2.days.to_i}"
    }
  else
    config.action_controller.perform_caching = false

    config.cache_store = :null_store
  end

  # ... omitted
end

Setting up our hello controller

We will have a basic controller action to demonstrate the cache. It will return some JSON.

class HelloController < ApplicationController
  def index
    res = Rails.cache.fetch(:cached_result) do
      # Only executed if the cache does not already have a value for this key

      # We will sleep for 3 seconds to simulate an expensive operation
      sleep 3

      # Return the array ["Hello", "World"]
      messages = %w[Hello World]
    end

    render json: { message: res }
  end
end

We use Rails.cache.fetch to look for the key cached_result in the cache. If it is not found, we will execute the block and store the result in the cache.

Our block has a sleep 3 line to simulate an expensive operation. The idea is that if we miss the cache, we will need to wait 3 seconds for a response.

With this done, we are now ready to ensure the route is available.

Update our routes

Set config/routes.rb to the following:

Rails.application.routes.draw do
  resources :hello, only: [:index]
end

We will now be able to access the GET request for /hello.

Running the server and testing our cache

We are now ready to run our server and test the cache setup. We need to ensure that caching is turned on for this.

# Toggle on the dev cache
$ bin/rails dev:cache

# Start the server
$ bin/rails s

In one terminal will be using HTTPie to demonstrate the requests so we can visually see the cache in action.

In another terminal, I will run redis-cli monitor to monitor anything that is entered into the cache and check that the configuration is correct.

Missing the cache

In my first call, I will be missing the cache. The logs look as follows:

$ http GET localhost:3000/hello
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"d75d122ce074e5382e16eaf331afa72a"
Referrer-Policy: strict-origin-when-cross-origin
Server-Timing: start_processing.action_controller;dur=0.125732421875, cache_read.active_support;dur=0.423095703125, cache_generate.active_support;dur=3003.9150390625, cache_write.active_support;dur=0.537841796875, process_action.action_controller;dur=3006.31298828125
Transfer-Encoding: chunked
Vary: Accept
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-Request-Id: 19bbeec4-189e-4593-8003-f6c567ce19d8
X-Runtime: 3.012563
X-XSS-Protection: 0

{
    "message": [
        "Hello",
        "World"
    ]
}

You will note here that it took 3.012563 seconds to get the response.

Note: It is outside the scope of this demo, but it might be worth adding another heading X-Cache to indicate that the cache was missed or hit. It is not a standard HTTP header field, but useful for debugging.

The Rails server console tells us the following:

Started GET "/hello" for ::1 at 2022-03-03 11:43:49 +1000
Processing by HelloController#index as */*
Completed 200 OK in 3006ms (Views: 0.3ms | ActiveRecord: 0.0ms | Allocations: 279)

This similarly tells us that it took 3 seconds to get the response.

Hitting the cache

At this point, our cache value will have been set in Redis during the first request.

If we run the request for a a second time, we get the following:

http GET localhost:3000/hello
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"d75d122ce074e5382e16eaf331afa72a"
Referrer-Policy: strict-origin-when-cross-origin
Server-Timing: start_processing.action_controller;dur=0.126953125, cache_read.active_support;dur=0.804931640625, cache_fetch_hit.active_support;dur=0.00634765625, process_action.action_controller;dur=2.620849609375
Transfer-Encoding: chunked
Vary: Accept
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-Request-Id: 279b4ba3-e72c-4893-ae5f-30a5d884f6a0
X-Runtime: 0.010306
X-XSS-Protection: 0

{
    "message": [
        "Hello",
        "World"
    ]
}

Note here that now the request took 0.010306 seconds.

The Rails server logs tell us the following:

Started GET "/hello" for ::1 at 2022-03-03 11:42:35 +1000
Processing by HelloController#index as */*
Completed 200 OK in 2ms (Views: 0.3ms | ActiveRecord: 0.0ms | Allocations: 207)

Awesome! We got the response from the cache.

If we were running redis-cli monitor that entire time, then the monitor logs tell us the following:

$ redis-cli monitor
OK
1646271829.959654 [1 [::1]:56084] "get" "cached_result"
1646271832.964237 [1 [::1]:56084] "set" "cached_result" "\x04\bo: ActiveSupport::Cache::Entry\t:\x0b@value[\aI\"\nHello\x06:\x06ETI\"\nWorld\x06;\aT:\r@version0:\x10@created_atf\x060:\x10@expires_in0"
1646271928.797673 [1 [::1]:56084] "get" "cached_result"

The set cached_result log was done to set the cache value. The get cached_result log was done to get the cache value (if it existed).

Summary

Today's post demonstrated how to set up the basics for the Rails cache with Redis.

It will be used again in future when we look at rate-limiting approaches.

If you wanted to go further, it is worth looking into passing arguments like expires_in to test different cache configurations.

Resources and further reading

Photo credit: anniespratt