skip to content

You are keeping per-endpoint request counters grouped under one Redis key. How does HINCRBY behave when the key or the field does not exist yet, when the stored value is not a number, and how does it compare with reading the field and writing it back from the application?

level: middleimportance: should knowfreq 42%

answer

  1. missing key/field ⇒ treated as 0, created
  2. returns value AFTER increment
  3. non-integer or 64-bit overflow ⇒ error, value untouched
  4. single command ⇒ atomic; HGET+HSET loses updates
  5. TTL is on the key and HINCRBY never refreshes it

basics

~20 s

HINCRBY treats a missing key or field as 0, creates it, and returns the new value. It errors if the value is not a 64-bit integer or if the result would overflow. It is one atomic command, so unlike HGET-then-HSET it cannot lose concurrent increments.

solid answer

~50 s

`HINCRBY counters:api /login 1` creates the key and the field if either is missing, treating the absent value as `0`, and returns the value **after** the increment. Negative amounts decrement; there is no floor at zero. Errors: if the field holds something that is not a base-10 64-bit integer (including `"3.5"` or a value with whitespace), Redis returns `hash value is not an integer`. If the result would exceed the signed 64-bit range, it returns an overflow error — the value is left unchanged in both cases. `HINCRBYFLOAT` handles decimals and rejects `inf`/`nan`. Against read-modify-write: `HGET` then `HSET` is two commands with a gap in which another client can increment; both then write the same value and one increment vanishes. Redis executes each command to completion before the next, so a single `HINCRBY` cannot interleave — no `WATCH`, no Lua, no lock. One gotcha: TTL belongs to the key, and `HINCRBY` does not touch it, so a counter key can expire mid-window unless you set the TTL deliberately.

code

text · 11 lines
text
HINCRBY counters:api /login 1     # (integer) 1  - key and field created from 0
HINCRBY counters:api /login 5     # (integer) 6
HINCRBY counters:api /login -2    # (integer) 4  - no floor at zero

HSET    counters:api note "n/a"
HINCRBY counters:api note 1
# (error) ERR hash value is not an integer

# windowed counters: TTL belongs to the key, and HINCRBY never sets or refreshes it
HINCRBY counters:api:2026-08-14T10 /login 1
EXPIRE  counters:api:2026-08-14T10 7200 NX

go deeper

for a junior

Know that HINCRBY creates the key/field from zero, returns the new value, and is a single atomic command.

for a middle

Add the error cases (non-integer value, 64-bit overflow, WRONGTYPE), negative increments, HINCRBYFLOAT, and why HGET+HSET loses updates under concurrency.

for a senior

Bring in the operational detail: TTL lives on the key and is never refreshed, the EXPIRE … NX pattern or window-in-key-name, and when grouping counters in one key creates a hot shard.

for a principal

Use it to make the general point that Redis's atomic single-command primitives remove the need for locks, and that reaching for WATCH or Lua should be justified by genuinely multi-step logic.

## The command ``` HINCRBY key field increment ``` It adds a signed integer to the numeric value of a hash field and returns the value **after** the increment. It is the hash counterpart of `INCRBY` on a string key, and it lets you keep a whole family of counters — one per endpoint, per status code, per tenant — inside a single key. ## Missing key, missing field Redis treats absence as zero: - If the key does not exist, it is created as a hash containing just this field. - If the key exists but the field does not, the field is created with value `0` before incrementing. So `HINCRBY counters:api /login 1` on a fresh server returns `1`. There is no need to initialise counters, and no need for `HSETNX` first — the "create if absent" behaviour is built in and atomic with the increment. ## Type and range rules The stored value must parse as a **base-10 signed 64-bit integer**. If it does not — because it is `"abc"`, `"3.5"`, `" 3"` with a space, or an empty string — Redis replies with an error (`hash value is not an integer`) and the field is left untouched. It is also a `WRONGTYPE` error if the key exists but holds a string, list or set rather than a hash. Overflow is detected, not wrapped: if the result would fall outside the signed 64-bit range, the command errors (`increment or decrement would overflow`) and the value is unchanged. In practice request counters never approach 2^63, but a counter fed by a bug (or a huge increment) fails loudly instead of silently wrapping negative. Negative increments are ordinary decrements: `HINCRBY key field -1`. There is no clamping at zero — a counter can go negative, so "decrement on release" patterns need their own floor logic if going below zero is meaningless. `HINCRBYFLOAT key field increment` is the decimal variant. It parses the value as a double, accepts exponential notation, and errors on values that are not valid floats and on results that would be `inf` or `nan`. Its result is returned as a string with trailing zeroes trimmed. Note that a field written by `HINCRBYFLOAT` may no longer be a valid integer, so a later `HINCRBY` on the same field can fail. ## Why it beats read-modify-write The application-side alternative is: 1. `HGET counters:api /login` → `"41"` 2. add 1 in the client 3. `HSET counters:api /login 42` There is a window between steps 1 and 3. Redis executes commands one at a time, but *your two commands are not one unit* — another client can complete its own read-modify-write inside that window. Both clients read 41, both write 42, and one request goes uncounted. Under load this is not a rare race: it is proportional to concurrency, and counters drift low in exactly the periods you most care about measuring. `HINCRBY` collapses read, add and write into a single command that the server runs to completion before it starts another, so interleaving is impossible. That is why it needs no `WATCH`, no Lua script, and no distributed lock. The same argument covers `HSETNX` (atomic claim), `INCR`, and the other read-modify-write commands Redis provides — reaching for a lock where a single command exists is a design smell. ## TTL interaction — the common bug Expiry in classic Redis is a property of the key, not the field, and **`HINCRBY` neither creates nor refreshes a TTL**. Two consequences: - If you want a counter key to disappear at the end of a window, you must set the TTL explicitly. Since `HINCRBY` may have just created the key, the usual pattern is `HINCRBY` followed by `EXPIRE` in the same pipeline, or an `EXPIRE ... NX` so a later call does not keep pushing the deadline out. - If the key already carries a TTL, incrementing does **not** extend it — the counter expires on schedule mid-traffic, which is usually what you want for a fixed window and surprising if you assumed activity kept it alive. A cleaner variant for time windows is to put the window in the key name (`counters:api:2026-08-14T10`), set the TTL once when the key is created, and let the whole window expire as a unit. Redis 7.4's `HEXPIRE` allows per-field TTLs, but for counters the window-in-the-key-name pattern is usually simpler. ## Grouping trade-off One key with many counter fields is memory-efficient (small hashes use a compact encoding), keeps related counters together for a single `HGETALL` read, and gives you one TTL for the whole group. The cost is that a key lives on one shard, so an extremely hot counter group concentrates its write load on a single node; splitting by tenant or window into several keys spreads it.

  • Does HINCRBY refresh the key's TTL?
    No. Expiry is a property of the key and increment commands leave it untouched, so a counter key expires on its original schedule even under continuous traffic. If the counter should live for a fixed window, set the TTL explicitly after the first increment — commonly with `EXPIRE … NX` so repeated calls do not keep extending the deadline — or encode the window in the key name and expire the whole key.
  • When would you still need a Lua script instead of plain HINCRBY?
    When the operation is conditional or spans more than one command as a unit: increment only if the current value is below a limit, increment two fields and read a third consistently, or increment and set a TTL as one indivisible step. A single `HINCRBY` is already atomic, so a script adds nothing for a bare increment — it earns its place only when the logic cannot be expressed as one command.

saying these in an interview costs you the question

  • Initialising the field with HSET 0 first, believing HINCRBY fails on a missing field
  • Expecting a non-integer value to be coerced or reset to 0 instead of erroring
  • Assuming HINCRBY refreshes the key's TTL
  • Wrapping HINCRBY in WATCH/MULTI or a distributed lock 'to make it atomic'
  • Believing HGET followed by HSET is equivalent because Redis is single-threaded — the two commands are not one unit

context