ruby 88 lines · 4 tabs

Coalesced Counter Cache with Redis Buffering and Nightly Reconciliation in Rails

Shared by codesnips Aug 2026
4 tabs
class CounterFlushJob
  include Sidekiq::Job
  sidekiq_options queue: :counters, retry: 3

  def perform
    CounterBuffer.flush_all.each do |field, delta|
      next if delta.zero?

      model_name, attribute, id = field.split(":", 3)
      klass = model_name.constantize

      klass.update_counters(id.to_i, attribute.to_sym => delta)
    rescue ActiveRecord::RecordNotFound, NameError
      # Row (or model) went away between buffering and flush; drop the delta.
      next
    end
  end
end
4 files · ruby Explain with highlit

Counter caches in Rails are convenient but every increment issues its own UPDATE against the same row, and under load that turns a hot record (a viral post, a popular product) into a lock-contention hotspot. This snippet shows a common production pattern: absorb high-frequency increments into a Redis buffer, flush the coalesced deltas periodically, and run a nightly job that reconciles the cached count against the source of truth so drift never accumulates.

The CounterBuffer service in the first tab is a thin wrapper over Redis. increment uses HINCRBY to accumulate a signed delta per model/attribute/id in a single hash, so a thousand likes on one post become one field being bumped a thousand times in memory rather than a thousand row locks. flush_all snapshots the hash with HGETALL, deletes it in the same pipeline via MULTI/EXEC, and yields the parsed deltas. Reading and clearing atomically is the crucial detail — it guarantees increments arriving mid-flush are either fully captured or left for the next cycle, never lost or double-counted.

CounterFlushJob in the second tab drains the buffer on a schedule. For each key it parses the composite model:attribute:id field and applies the accumulated delta with a single update_counters call, which emits an atomic SET col = col + n rather than reading then writing. Zero deltas are skipped, and a record_not_found rescue quietly drops counters for rows that were deleted between buffering and flushing.

Because buffered systems can still drift — a lost flush, a crash between Redis clearing and the DB write, a manual data edit — CounterReconciliationJob in the third tab recomputes the truth. It walks records in batches with find_in_batches, counts the real associated rows with a GROUP BY, and only writes when the cached value disagrees, logging every correction. Running it nightly keeps errors bounded to a single day.

The Post model tab wires it together: bump_likes_counter writes to the buffer instead of touching the column directly, while likes_count stays a plain cached column that reads are free to trust. The trade-off is eventual consistency — the displayed count lags by up to one flush interval — in exchange for eliminating write contention. This pattern fits any counter that is written far more often than it must be exact to the millisecond.


Related snips

Share this code

Here's the card — post it anywhere.

Coalesced Counter Cache with Redis Buffering and Nightly Reconciliation in Rails — share card
Link copied