Mastering Redis Counters: INCR/INCRBY vs DECR/DECRBY and Robust Design Patterns

This article explains Redis's atomic counter commands (INCR, INCRBY, DECR, DECRBY), highlights their pitfalls such as missing TTL and concurrency issues, and presents practical design solutions—including SET NX+EX initialization, double‑TTL compensation, retry queues, fallback strategies, and performance optimizations like Lua scripting and pipelining.)

Ma Wei Says
Ma Wei Says
Ma Wei Says
Mastering Redis Counters: INCR/INCRBY vs DECR/DECRBY and Robust Design Patterns

01 INCR/INCRBY and DECR/DECRBY Commands

Redis provides four atomic counter commands that avoid race conditions in high‑concurrency scenarios. All have O(1) time complexity and can handle over 100k QPS per instance.

INCR : atomically increments the integer value of a key by 1. If the key does not exist, Redis creates it with value 0 before incrementing.

INCRBY : atomically adds a user‑specified increment (positive or negative) to the key. Missing keys are initialized to 0.

DECR : atomically decrements the integer value of a key by 1, initializing missing keys to 0 before decrementing.

DECRBY : atomically subtracts a user‑specified decrement (positive or negative) from the key.

Typical usage includes page‑view counters, like counts, inventory deduction, and leaderboard scores.

SET counter 10
INCR counter   # returns 11
INCR counter   # returns 12
INCR new_counter   # returns 1 (key created as 0 then +1)

SET counter 10
INCRBY counter 5   # returns 15
INCRBY counter -3  # returns 12
INCRBY new_counter 10   # returns 10

SET counter 10
DECR counter   # returns 9
DECR counter   # returns 8
DECR new_counter   # returns -1

SET counter 10
DECRBY counter 3   # returns 7
DECRBY counter -5   # returns 12 (subtracting a negative adds)
DECRBY new_counter 5   # returns -5

However, these commands have three major drawbacks:

Key never expires : Automatic initialization does not set a TTL, causing keys to persist indefinitely.

Concurrent initialization race : Multiple threads may simultaneously create the key, leading to data inconsistency.

Unmanaged failures : Redis outages or network glitches can cause operations to fail without a fallback mechanism.

02 Counter Design Solutions

2.1 SET NX+EX with Expiration

Use SET key 0 NX EX ttl to atomically create the key only if it does not exist and set an expiration time, then apply INCR:

public long increment(String key, long ttlSeconds) {
    RedisUtil.set(key, "0", "nx", "ex", ttlSeconds);
    return RedisUtil.incr(key);
}

This prevents keys from living forever and avoids race‑condition overwrites.

2.2 Double‑TTL Compensation Mechanism

Because SET NX+EX may still leave a key without TTL when multiple threads race, add two safeguards:

First‑increment compensation : If INCR returns 1, the key was just created without TTL; set the TTL explicitly.

TTL anomaly detection : After each increment, check TTL; if it returns -1, re‑apply the TTL.

public long increment(String key, long ttlSeconds) {
    RedisUtil.set(key, "0", "nx", "ex", ttlSeconds);
    long result = RedisUtil.incr(key);
    if (result == 1) {
        RedisUtil.expire(key, ttlSeconds);
    }
    if (RedisUtil.ttl(key) == -1) {
        RedisUtil.expire(key, ttlSeconds);
    }
    return result;
}

2.3 Retry Queue and Degradation Mechanism

Network glitches or master‑slave failover can cause Redis commands to fail. Implement a retry queue that re‑executes failed operations asynchronously, ensuring eventual consistency. If Redis becomes completely unavailable, fall back to a local counter or a database.

public long increment(String key, long ttlSeconds) {
    try {
        RedisUtil.set(key, "0", "nx", "ex", ttlSeconds);
        long result = RedisUtil.incr(key);
        if (result == 1) RedisUtil.expire(key, ttlSeconds);
        if (RedisUtil.ttl(key) == -1) RedisUtil.expire(key, ttlSeconds);
        return result;
    } catch (Exception e) {
        queue.offer(new RetryTask(key, ttlSeconds)); // async retry
        return 1; // fallback value, adjust as needed
    }
}

public long incrementWithFallback(String key, long ttlSeconds) {
    try {
        return increment(key, ttlSeconds);
    } catch (Exception e) {
        logger.warn("Redis unavailable, falling back to local counter");
        return localCounter.increment(key);
    }
}

2.4 Additional Performance Optimizations

Lua scripting : Combine SET NX+EX, INCR, EXPIRE, and TTL checks into a single atomic script to reduce network round‑trips.

Pipeline batch operations : Use Redis pipelines when incrementing many counters at once.

Hot‑key sharding : Distribute extremely hot keys across multiple slots (e.g., using hash tags) to avoid single‑key bottlenecks.

Multi‑level counters : Cache counts locally (e.g., with Caffeine) and periodically flush to Redis, reducing write pressure.

03 Final Summary

INCR/INCRBY and DECR/DECRBY are efficient atomic commands suitable for high‑concurrency counter scenarios. Simple increments (INCR/DECR) work for fixed‑step use cases, while INCRBY/DECRBY support flexible batch adjustments. The presented design patterns—atomic initialization with TTL, double‑TTL compensation, retry queues, fallback strategies, and performance tweaks—address the inherent pitfalls of missing expiration, race conditions, and failure handling. Choose the combination that best fits your business requirements and technology stack; Redis is a powerful option but not the only possible solution.

TTLCounterhigh-concurrencydecrdecrbyincrincrby
Ma Wei Says
Written by

Ma Wei Says

Follow me! Discussing software architecture and development, AIGC and AI Agents... Sometimes sharing insights on IT professionals' life experiences.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.