What’s a safe pattern for real-time counters (likes, views) that won’t double-count under concurrency?
In-Memory Databases & Caching

What’s a safe pattern for real-time counters (likes, views) that won’t double-count under concurrency?

9 min read

Most real-time counters break down the moment your app gets popular: two users tap “like” at the same time, one increment gets lost, and your numbers quietly drift from reality. The fix is to move counting into a fast, atomic “memory layer” like Redis, and use patterns that avoid double-counting even under heavy concurrency.

Quick Answer: Use Redis as your real-time counter engine with atomic increments (INCR, HINCRBY, INCRBYFLOAT) or transactions, and track “who already liked what” in a separate idempotency structure (sets or bitmaps). This keeps counts accurate under concurrency, replays, and retries, while still delivering sub-millisecond latency at scale.


The Quick Overview

  • What It Is: A Redis-backed pattern for real-time counters that uses atomic operations, idempotency keys, and optional time-bucketing to give you accurate likes, views, and rate limits under concurrency.
  • Who It Is For: Backend engineers, SREs, and platform teams running APIs, social feeds, ad systems, or AI apps that need low-latency counters without overloading their primary database.
  • Core Problem Solved: Avoids lost updates and double-counts when many clients increment the same counter at the same time, while keeping the system simple to operate and cheap to scale.

How It Works

At a high level, you separate “did this user perform this action?” from “how many times did this happen?” and let Redis handle both with atomic primitives:

  1. Atomic increment for the aggregate counter.
    Use INCR/INCRBY/HINCRBY so concurrent updates never overwrite each other.

  2. Idempotency check for per-user actions.
    Use a Redis Set, Bitmap, or similar to record “user X already liked item Y.” Before incrementing, check this structure; if the user already liked, skip the INCR.

  3. (Optional) Time-bucketing for trends and limits.
    For rate limits or trending charts, write to time-scoped keys (per minute/hour/day) with expiration; Redis handles TTL, you aggregate buckets as needed.

Because Redis runs as your fast memory layer across Redis Cloud, Redis Software, or Redis Open Source, all of this happens with sub-millisecond latency and atomic semantics—even with thousands of concurrent clients.


  1. Phase 1 – Choose the right counter structure

    Pick based on cardinality and access patterns:

    • Single integer key for “global counters”
      views:article:1234 → integer; use INCR/INCRBY.

    • Hash field per entity for many counters in a namespace
      views:articles → hash with fields 1234, 1235, …; use HINCRBY.

    • Float counters for weighted events (e.g., scoring)
      Use INCRBYFLOAT on keys or hash fields.

    Example (Go, using go-redis):

    val, err := rdb.Incr(ctx, "views:article:1234").Result()
    if err != nil {
        panic(err)
    }
    fmt.Printf("Views for 1234: %d\n", val)
    
  2. Phase 2 – Add idempotency so concurrency can’t double-count

    The key idea: before incrementing, ensure this specific actor-event combination hasn’t already been counted.

    Common patterns:

    • Likes (one per user per item):
      • Use a Set keyed by item, value is user ID.
      • Use SADD with a check on “added” vs “already present.”
    • Binary actions (voted, subscribed, etc.):
      • Use a Bitmap keyed by item, offset is user ID or hashed user ID.
      • Use SETBIT and read the previous bit.

    Example (likes using Set + counter):

    -- Lua script to ensure idempotent like
    -- KEYS[1] = likes:set:item:{itemId}
    -- KEYS[2] = likes:count:item:{itemId}
    -- ARGV[1] = userId
    
    if redis.call("SADD", KEYS[1], ARGV[1]) == 1 then
        -- First time this user likes the item
        return redis.call("INCR", KEYS[2])
    else
        -- Already liked; return current count without increment
        return redis.call("GET", KEYS[2])
    end
    

    Then call via your client:

    # Example using redis-cli
    EVALSHA <script_sha> 2 likes:set:item:1234 likes:count:item:1234 user-42
    

    This script runs atomically on the Redis server: set membership check + increment happen as a single, safe operation even under heavy concurrency.

  3. Phase 3 – Time-bucket and expire where it makes sense

    For views, rate limits, or time-based dashboards, use time-bucketed keys with TTL:

    • Key pattern: views:article:{id}:{yyyyMMddHHmm}
    • Use INCR and EXPIRE together, ideally in a transaction/Lua script so they’re atomic.

    Example (pseudocode for per-minute rate limit):

    key = "ratelimit:" + apiKey + ":" + currentMinute()
    
    MULTI
      INCR key
      EXPIRE key 60
    EXEC
    
    if INCR_result > LIMIT:
        reject request
    

    Note: In Redis, INCR on a non-existent key returns 1, so the first call in the window always starts at 1. Putting INCR and EXPIRE in the same MULTI/EXEC makes the operation atomic; either both are applied, or neither is.

    Warning: If the Redis server crashes between INCR and EXPIRE and the transaction is not yet complete, that INCR will not be restored from AOF/in-memory replication. This is rare but worth knowing; for strict durability on counters, consider periodic background compaction to a system of record.


Features & Benefits Breakdown

Core FeatureWhat It DoesPrimary Benefit
Atomic counter operationsUses INCR, HINCRBY, INCRBYFLOAT to update counters without race conditions.No lost updates even when thousands of clients increment simultaneously.
Idempotent like/view patternsCombines sets/bitmaps with counters via Lua/transactions for “count once per user.”Prevents double-counting under retries, replays, and concurrent clicks.
Time-bucketed keys with TTLStores counters per minute/hour/day with expiration.Real-time trends and rate limits with automatic cleanup.
Deploy anywhere with Redis Cloud/Software/Open SourceLets you run the same pattern on fully managed Redis Cloud, on‑prem Redis Software, or Redis Open Source.Consistent architecture across clouds, Kubernetes, and bare metal.
Observability via Prometheus/GrafanaExposes v2 metrics and latency histograms for Redis operations.Operational safety: track p99/p99.9 counter latency and capacity.

Ideal Use Cases

  • Best for social likes, reactions, and upvotes:
    Because a Set or Bitmap + atomic counter guarantees each user is counted once, even if your mobile app retries or users hammer the button.

  • Best for views, impressions, and rate limits:
    Because INCR on time-bucketed keys with TTL lets you track per-minute/hour/day counts and enforce limits without hitting your primary database.

  • Best for AI agent memory and usage tracking:
    Because Redis can act as both vector database (for semantic memory) and a high-throughput counter engine (for token usage, conversation turns, or agent events) in the same fast memory layer.


Limitations & Considerations

  • Counters are eventually persisted, not transactional with your primary DB:
    Redis gives you atomic operations inside Redis, but it’s not a full transactional system with your system of record. If you need invoices or billing-grade precision, periodically aggregate counters into your primary database and treat Redis as the real-time layer.

  • Key cardinality and memory usage:
    Sets like likes:set:item:{id} grow with users; time-bucketed keys grow with traffic. Use:

    • Expiration (EXPIRE) on time-based keys.
    • Periodic compaction (aggregate and delete old keys).
    • Bitmap/HyperLogLog when you can trade precision for memory.
  • Durability vs speed tradeoff:
    Heavy use of counters with very high write rates can increase AOF size and replication load. Tune:

    • AOF rewrite settings.
    • Memory limits and eviction policies.
    • Cluster sharding to distribute hotspot keys.
  • Correctness under client-side batching:
    If you batch increments client-side and send them later, you can still get correct totals, but you won’t be strictly “real-time” for every event. Use this for cost control, not where immediate accuracy is critical.


Pricing & Plans

Redis counters work the same across Redis Cloud, Redis Software, and Redis Open Source; pricing differences come from how you deploy and scale:

  • Redis Cloud (fully managed):
    Pay for the memory and throughput you need, with automatic clustering, failover, and metrics. Best if you want to offload operations and just focus on building.

  • Redis Software (on‑prem/hybrid):
    License Redis for your own infrastructure (Kubernetes, VMs, bare metal). Best if you have strict data residency, security, or integration requirements.

  • Redis Open Source:
    Download Redis 8 and run it yourself. Best for experimentation, small workloads, or when you want full control and are comfortable managing operations.

Within Redis Cloud and Redis Software, you’ll typically pick a plan size based on:

  • Peak ops/sec for INCR/HINCRBY.

  • Memory footprint of counters + idempotency sets/bitmaps.

  • Required uptime (automatic failover, Active-Active Geo Distribution for 99.999% uptime and local sub-millisecond latency).

  • Growth / Dev Plan: Best for teams validating the pattern, development environments, and moderate traffic apps that need real-time counters but not yet at massive scale.

  • Production / Enterprise Plan: Best for high-traffic social/ads/AI workloads needing high throughput, strict SLAs, Active-Active Geo Distribution, and deep observability via Prometheus/Grafana.


Frequently Asked Questions

How do I prevent double-counting when multiple servers process the same like event?

Short Answer: Use an idempotency key in Redis—typically a Set or Bitmap per item—and only increment the counter if the idempotency entry is new.

Details:
When multiple instances of your service may receive duplicate events (retries, queue redelivery, cross-region processing), treat each “user likes item” as a logical idempotent operation. The canonical pattern:

  1. Build a key like likes:set:item:{itemId}.
  2. Run SADD likes:set:item:{itemId} {userId}.
  3. If the return value is 1, this user is new → INCR likes:count:item:{itemId}.
  4. If the return value is 0, they already liked → do not increment.

Wrap steps 2–3 in a Lua script or MULTI/EXEC so they execute atomically. This makes the “like” operation safe against duplicates and concurrent updates.


Are Redis counters safe under heavy concurrency and failover?

Short Answer: Atomic operations like INCR are safe under concurrency; Redis ensures each increment is applied exactly once. During failover, the usual Redis persistence rules apply, so a small window of the most recent increments can be lost if you don’t configure durability appropriately.

Details:
Within a single Redis primary node:

  • INCR, HINCRBY, INCRBYFLOAT are atomic.
  • There’s no race condition between concurrent clients; Redis is single-threaded per core for command execution.

Under replication and failover:

  • If a primary fails before an operation is replicated or written to AOF, that last bit of state can be lost.
  • Active-Active Geo Distribution gives you multi-region resilience with CRDT-like semantics for some data types, but the simple counter pattern here assumes standard primary-replica.

To operate safely:

  • Enable AOF with a suitable fsync policy (everysec is a common balance).
  • Monitor replication lag.
  • Use Prometheus/Grafana with Redis’s latency histograms to track p99/p99.9 for increment operations and watch for spikes around failover events.

If you need bulletproof, audit-grade counts (e.g., billing), treat Redis as the real-time layer and periodically reconcile counters into your primary system of record.


Summary

Accurate real-time counters under concurrency come down to three rules:

  1. Use Redis atomic operations (INCR, HINCRBY, INCRBYFLOAT) instead of “read-modify-write” in your app.
  2. Make each logical action idempotent with Sets or Bitmaps so a user can’t be counted twice—no matter how many times they hit “like” or your services retry.
  3. Time-bucket and expire where appropriate for rate limiting and trend analytics, then aggregate upstream for long-term storage.

Redis gives you the fast memory layer, data structures, and operational surface (clustering, automatic failover, metrics) to make this pattern robust at scale—without hammering your primary database.


Next Step

Get Started