ruby 74 lines · 3 tabs

Targeted Query Caching for Expensive Endpoints

Shared by codesnips Jan 2026
3 tabs
module Cacheable
  extend ActiveSupport::Concern

  def cache_query(prefix, scope, expires_in: 15.minutes)
    key = [prefix, cache_version_token(scope)].join("/")

    Rails.cache.fetch(key, expires_in: expires_in, race_condition_ttl: 10.seconds) do
      yield
    end
  end

  private

  def cache_version_token(scope)
    relation = scope.unscope(:order)
    max = relation.maximum(:updated_at)
    stamp = max ? max.utc.to_f.to_s : "0"
    "#{relation.count}-#{stamp}"
  end
end
3 files · ruby Explain with highlit

This snippet shows how an expensive read-heavy endpoint in Rails can be cached with a versioned key strategy that survives model updates without manual key bookkeeping. The core problem is that a leaderboard-style aggregation query is slow and hit constantly, but its results only change when the underlying records change. Rather than caching the whole HTTP response, the caching is scoped to the query result, keyed off data that naturally advances when the data mutates.

In Cacheable concern, cache_query wraps Rails.cache.fetch and builds the key from a caller-supplied prefix plus a cache_version_token. That token is a maximum(:updated_at) combined with a count, so any insert, update, or delete on the scope produces a new key and the old entry is simply orphaned instead of needing explicit deletion. The race_condition_ttl option is important: it lets one process keep serving a slightly-stale value while another recomputes, preventing a thundering herd from hammering the database the instant a popular key expires.

In LeaderboardQuery, the actual aggregation lives in records, a plain query object that groups scores and orders them. It calls cache_query with a prefix that includes the season_id, so different seasons occupy distinct keys and invalidate independently. The since filter is folded into the key too, ensuring different time windows never collide. Keeping the SQL in a query object keeps the concern generic and the controller thin.

In LeaderboardsController, index delegates entirely to the query object and renders JSON. Because caching happens beneath the controller, the same cached data can be reused by jobs, exports, or other endpoints without duplicating cache logic. HTTP-level stale? handling with an etag is layered on top so unchanged responses short-circuit with a 304.

The main trade-off is that maximum(:updated_at) and count are themselves queries, though far cheaper than the aggregation they guard, and they can be pushed into a covering index. A pitfall to watch is clock precision: if updated_at lacks sub-second resolution, two rapid writes could share a token, which is why count is mixed in as a tiebreaker. This pattern fits endpoints where reads vastly outnumber writes and exact freshness within milliseconds is not required.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Targeted Query Caching for Expensive Endpoints — share card
Link copied