class ShardedCounter
SHARDS = 16
SNAPSHOT_TTL = 10 # seconds
def initialize(name, redis: REDIS)
@name = name
@redis = redis
end
def increment(by = 1)
shard = rand(SHARDS)
@redis.hincrby(shards_key, shard, by)
end
def approximate_value
cached = @redis.get(snapshot_key)
return cached.to_i if cached
live_sum
end
def refresh_snapshot!
total = live_sum
@redis.set(snapshot_key, total, ex: SNAPSHOT_TTL)
total
end
def shards_key
"counter:#{@name}:shards"
end
def snapshot_key
"counter:#{@name}:snapshot"
end
private
def live_sum
@redis.hgetall(shards_key).values.sum(&:to_i)
end
end
class CounterFlushJob < ApplicationJob
queue_as :low
# Enqueued on a schedule; recomputes approximate snapshots off the read path.
def perform(*counter_names)
names = counter_names.presence || tracked_counters
names.each do |name|
counter = ShardedCounter.new(name)
total = counter.refresh_snapshot!
Rails.logger.info("[counter-flush] #{name}=#{total}")
end
end
private
def tracked_counters
REDIS.smembers("counter:registry")
end
end
class MetricsController < ApplicationController
def show
counter = ShardedCounter.new(params[:id])
render json: { key: params[:id], value: counter.approximate_value }
end
def track
counter = ShardedCounter.new(params[:id])
REDIS.sadd("counter:registry", params[:id])
counter.increment(view_weight)
head :accepted
end
private
def view_weight
params.fetch(:weight, 1).to_i.clamp(1, 100)
end
end
The problem this snippet addresses is contention on a single hot counter: when thousands of requests per second try to increment one value, a single Redis key or a single database row becomes a serialization point. Every writer contends for the same lock or the same key, and throughput collapses even though the operation is trivial. The classic fix is to trade exactness for scalability by sharding the counter across N sub-keys and accepting that reads are approximate for a short window.
In ShardedCounter, each logical counter is spread over a fixed number of Redis shards chosen at random on every write. Because writers land on different shards, INCRBY operations spread across independent keys and no single key is a bottleneck. The #increment method picks a shard with rand and issues an atomic hincrby, so there is no read-modify-write and no application-level lock — the increment is lock-free from the caller's perspective. The #approximate_value method sums a cached snapshot rather than touching every shard on the read path, which is where the "approximate" trade-off lives: the returned number may lag real-time writes by up to the cache TTL.
The read path is deliberately cheap. #approximate_value reads a single pre-aggregated key populated by a background job, falling back to a live shard sum only on a cache miss. This keeps hot reads O(1) instead of O(shards), and the small staleness is acceptable for dashboards, rate displays, and trending metrics where exactness is not required.
In CounterFlushJob, a periodic job walks every shard with hgetall, sums the values, and writes the aggregate back to the snapshot key with a TTL. Running the aggregation off the request path means readers never pay for the fan-out. The job is idempotent — re-running it simply recomputes the same sum — so overlapping runs are harmless.
MetricsController shows the wiring: writes call #increment directly and never block, while reads serve the cached approximation. The main pitfall is choosing shard count: too few reintroduces contention, too many slows the flush. This pattern fits view counts, likes, and telemetry — anywhere write volume is high and a few seconds of drift is invisible to users.
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
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
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
<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)
Share this code
Here's the card — post it anywhere.