skip to content

Why is Redis's INCR the correct way to implement a counter shared by many clients, and what happens if the key is missing, holds non-numeric text, or is incremented past the 64-bit range?

level: middleimportance: must knowfreq 68%

answer

  1. read-modify-write executed server-side, one command
  2. missing key = 0, first INCR returns 1
  3. non-numeric → error, value untouched, never resets
  4. signed 64-bit, overflow errors instead of wrapping
  5. INCR keeps the TTL; SET destroys it

basics

~20 s

INCR performs the read-modify-write inside Redis as one command, so concurrent clients cannot lose updates. A missing key is treated as 0 and created. A non-numeric or out-of-range value returns an error rather than resetting. Values are signed 64-bit; overflow errors instead of wrapping. INCR keeps any existing TTL.

solid answer

~50 s

A counter is a read-modify-write. Doing it client-side (`GET`, add one, `SET`) loses updates: two clients read 7 and both write 8. `INCR key` moves the whole operation into the server, and since Redis runs one command to completion before the next, the increment is atomic without any locking. Semantics worth knowing: - **Missing key** → treated as `0`, so the first INCR returns 1 and creates the key. - **Non-numeric value** → error `value is not an integer or out of range`; the value is untouched. - **Range** → signed 64-bit; passing `LLONG_MAX` errors, it does not wrap. - **TTL** → preserved. INCR modifies the value, it does not recreate the key, so an expiring counter keeps counting down to its original deadline. `INCRBY`/`DECRBY` take a step, `INCRBYFLOAT` works on floating values (and has no DECRBYFLOAT — pass a negative). The reply is the new value, which is what makes `INCR` usable as a rate limiter or ID generator: you act on the number it hands back.

code

text · 16 lines
text
> INCR visits            # key absent → treated as 0
(integer) 1
> INCRBY visits 10
(integer) 11
> SET visits "eleven"
OK
> INCR visits
(error) ERR value is not an integer or out of range

# fixed-window limiter: arm the TTL only on the window's first hit
> INCR rl:user42:20260814T1203
(integer) 1
> EXPIRE rl:user42:20260814T1203 60
(integer) 1
> INCR rl:user42:20260814T1203    # ttl untouched by INCR
(integer) 2

go deeper

for a junior

Know that INCR is atomic, creates the key from 0, and returns the new value.

for a middle

State the error behaviour on non-numeric and overflow values, and that INCR preserves the TTL while SET destroys it.

for a senior

Explain the rate-limiter idiom including the INCR==1 TTL arming, the residual two-command gap, and when to close it with Lua.

for a principal

Position counters as fast in-memory aggregates: atomic but only as durable as the persistence and replication settings allow, so reconcile anything financially meaningful against a system of record.

## The problem INCR solves Incrementing a shared number is the textbook lost-update scenario. If two clients each `GET counter` (both see 7), each adds one locally, and each `SET counter 8`, one increment vanished. No amount of client speed fixes it — the read and the write are separate operations with a gap in between. Redis's answer is to ship the operation, not the data: `INCR counter` tells the server to do the whole read-modify-write itself. Because Redis processes commands one at a time to completion, no other client's command can interleave inside it, so the increment is atomic with no locks, no retries and one round trip. ## Exact semantics **Missing key.** INCR on a key that does not exist treats the current value as 0, so it returns 1 and creates the key. This is why counters need no initialisation step — the first hit creates them. The same is true of INCRBY, DECR, DECRBY and INCRBYFLOAT. **Wrong content.** Redis strings are byte arrays; INCR parses the stored bytes as a base-10 signed integer each time. If the value is `"abc"`, has leading/trailing spaces, or is a float like `"1.5"`, the command fails with `ERR value is not an integer or out of range` and the stored value is left exactly as it was. It never silently resets to zero. If the key holds a different *type* (a list, a hash), you get `WRONGTYPE` instead. **Range and overflow.** The counter is a signed 64-bit integer: −9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. Incrementing past the maximum returns the same "not an integer or out of range" error rather than wrapping around. If you need wrap-around or a narrower width, that is what BITFIELD's typed counters with OVERFLOW WRAP/SAT are for. **Floats.** `INCRBYFLOAT key 0.1` uses long-double arithmetic and reformats the stored value, stripping trailing zeros. It rejects `nan` and `inf` results with an error. There is deliberately no DECRBYFLOAT — you pass a negative increment. Be aware that the value is stored as text, so repeated float increments carry the usual binary-floating-point rounding; for money, count integer minor units with INCRBY instead. **TTL interaction.** This trips people up in rate limiters. A full `SET` clears the key's expiry; `INCR` does not. So the standard fixed-window limiter is: `INCR ratelimit:{user}:{minute}` and, only when the reply is exactly 1 (meaning you just created the window), `EXPIRE ratelimit:{user}:{minute} 60`. Every later increment inside the window leaves the countdown alone. Because those are two commands, a crash between them can leave a window key without expiry — embedding both in a small Lua script or using `SET ... NX EX` to seed the window removes that gap. ## Why the return value matters INCR replies with the value *after* the increment. That single fact makes it a general coordination primitive: - **Rate limiting** — act when the reply exceeds the quota. - **Sequence/ID generation** — the reply is your unique, monotonically increasing id (unique per key, and only as durable as your persistence settings, so do not treat it as a gapless database sequence). - **Idempotent fan-in** — increment a per-batch counter and let whoever receives the final expected number finish the job. - **Reference counting** — DECR and act when the reply hits 0. Because the whole decision is derived from an atomic reply, there is no window in which two clients both believe they were the winner. ## Common mistakes The first is reintroducing the race by reading afterwards: `INCR k` then `GET k` to "confirm" the value gives you someone else's number. Always use the reply. The second is thinking a MULTI/EXEC transaction around GET and SET fixes the lost update — queued commands cannot see each other's results, so you would need WATCH-based optimistic concurrency, which retries under contention; INCR simply avoids the problem. The third is assuming that because INCR is atomic, a *sequence* of INCRs across several keys is atomic — it is not; that requires a script. ## Durability caveat Atomic does not mean durable. An acknowledged INCR lives in memory and reaches disk only according to your persistence configuration, and replicas are updated asynchronously, so a counter can move backwards after an unclean failover. For billing-grade counting, treat Redis's counter as a fast aggregate and reconcile against a durable store.

  • Two clients run INCR on the same key at the same moment. Can they both receive the same reply?
    No. Redis executes each command to completion before starting the next, so the two increments are serialised and the replies are distinct consecutive values. That is precisely why INCR's return value can be used as a unique ticket number or a rate-limit decision — no two callers can win the same slot.
  • Why does a rate limiter arm its TTL only when INCR returns 1?
    A reply of 1 means the key did not exist, so this call created the window and should set its lifetime. Calling EXPIRE on every hit would slide the window forward and let a steady stream of requests keep it alive indefinitely. INCR itself never touches the TTL, so later hits within the window need no expiry handling at all.
  • Would wrapping GET and SET in MULTI/EXEC give you the same safety as INCR?
    No. Commands queued in a transaction cannot use each other's results, so you cannot compute value+1 inside the transaction. You would need WATCH on the key plus a client-side retry loop when the transaction aborts — correct but slower and more complex under contention. INCR removes the race entirely by doing the arithmetic server-side.

saying these in an interview costs you the question

  • Doing GET, add one, SET from the client and calling it atomic because Redis is fast
  • Believing INCR on a non-numeric value resets the counter to zero or to one
  • Expecting 64-bit overflow to wrap around silently
  • Calling EXPIRE on every increment in a rate limiter, which makes the window slide
  • Reading the key again after INCR instead of using the command's reply

context