ruby 81 lines · 3 tabs

Lock-Free Read Pattern for Hot Counters (Approximate)

Shared by codesnips Jan 2026
3 tabs
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
3 files · ruby Explain with highlit

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

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.

Lock-Free Read Pattern for Hot Counters (Approximate) — share card
Link copied