class LeaderboardCache
TOP_KEY = "leaderboard:top".freeze
STATS_KEY = "leaderboard:stats".freeze
def top_players
Rails.cache.fetch(TOP_KEY, expires_in: 5.minutes, race_condition_ttl: 15.seconds) do
Player.order(score: :desc).limit(50).to_a
end
end
def expensive_stats
cached = Rails.cache.read(STATS_KEY)
return cached if cached
lock = RedisLock.new("lock:#{STATS_KEY}", ttl: 30.seconds)
if lock.acquire
begin
value = compute_stats
Rails.cache.write(STATS_KEY, value, expires_in: 10.minutes)
value
ensure
lock.release
end
else
lock.wait_for_result { Rails.cache.read(STATS_KEY) } || compute_stats
end
end
private
def compute_stats
{
total: Player.count,
average: Player.average(:score).to_f.round(2),
generated_at: Time.current
}
end
end
class RedisLock
RELEASE_SCRIPT = <<~LUA.freeze
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA
def initialize(key, ttl:, redis: Redis.current)
@key = key
@ttl_ms = (ttl.to_f * 1000).to_i
@token = SecureRandom.hex(16)
@redis = redis
end
def acquire
@redis.set(@key, @token, nx: true, px: @ttl_ms) ? true : false
end
def release
@redis.eval(RELEASE_SCRIPT, keys: [@key], argv: [@token])
end
def wait_for_result(attempts: 20, interval: 0.1)
attempts.times do
result = yield
return result if result
sleep(interval)
end
nil
end
end
class StatsController < ApplicationController
def leaderboard
cache = LeaderboardCache.new
render json: {
top: cache.top_players.map { |p| { id: p.id, name: p.name, score: p.score } },
stats: cache.expensive_stats
}
end
end
A cache stampede (or "dogpile") happens when a popular cache entry expires and dozens of concurrent requests all miss at once, each racing to recompute the same expensive value against the database. This snippet shows two complementary defenses in a Rails app: the built-in race_condition_ttl option and an explicit distributed lock for the truly expensive cases.
The LeaderboardCache service wraps Rails.cache.fetch with expires_in and race_condition_ttl. The trick behind race_condition_ttl is that ActiveSupport stores the value with a slightly longer physical lifetime than the logical expires_in. When the logical entry expires, the first reader that misses immediately re-writes the stale value with a short future expiry (the race_condition_ttl window) so other concurrent readers keep getting the stale-but-valid value while one process recomputes. This bounds the number of simultaneous recomputes to roughly one, at the cost of briefly serving slightly stale data — an acceptable trade for a leaderboard.
race_condition_ttl only helps for entries that already exist and expire; it does nothing for a cold cache or a brand-new key, where every process misses simultaneously. For those genuinely expensive computations, expensive_stats in the same service reaches for with_lock, backed by the RedisLock helper in the second tab. RedisLock#acquire uses SET key value NX PX ttl — an atomic "set if not exists" with an expiry — so exactly one process wins the lock. Losers call wait_for_result, polling the cache briefly rather than piling onto the database. The lock is released with a Lua compare-and-delete so a process never deletes a lock it no longer owns (important if computation overran the TTL).
Notice the deliberate ordering in expensive_stats: check cache, try the lock, recompute only if the lock is won, and always re-check the cache after waiting. The ensure block guarantees the lock is freed even on error. The StatsController ties it together, simply calling the service and rendering — all stampede handling lives behind the cache layer. The pitfalls worth remembering: pick a race_condition_ttl comfortably larger than the recompute time, keep lock TTLs above worst-case compute duration, and never trust a lock you can't prove you still hold.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
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
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
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
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
Share this code
Here's the card — post it anywhere.