Skip to main content
Redis · lesson 2 of 14
Redis beginner Lesson 2 of 14

Expiry, Atomic Counters, and the -1 vs -2 TTL Trap

TTL returns -1 for a key that never expires and -2 for one that's already gone — mixing those up is the most common expiry bug. Then: INCR's atomic counters, and the error when a string isn't a number.

By SkillByExample · Published

redis strings expiry ttl atomic-counters

Two things about strings look simple and have a real trap hiding in each: expiry, and treating a string as a number.

Setting a value that expires

docker exec redis-lesson redis-cli SET session:abc "active" EX 5
docker exec redis-lesson redis-cli TTL session:abc
docker exec redis-lesson redis-cli GET session:abc
OK
4
active

EX 5 sets the key to expire in 5 seconds. TTL shows 4 a moment later — it’s a countdown, not a fixed snapshot, so seeing it a second lower than what you set is normal.

The bug: confusing “no expiry” with “gone”

docker exec redis-lesson redis-cli SET permanent "value"
docker exec redis-lesson redis-cli TTL permanent
OK
-1
# after session:abc's 5 seconds have passed
docker exec redis-lesson redis-cli GET session:abc
docker exec redis-lesson redis-cli TTL session:abc

-2

Two different negative numbers, two completely different situations. permanent has -1 because it was never given an expiry — it isn’t going anywhere. session:abc has -2 because it expired and no longer exists at all — GET on it returns nothing. Code that treats -1 and -2 the same way (say, “refresh if TTL is negative”) will pointlessly regenerate a value that was never expiring in the first place.

EXPIRE and PERSIST on an existing key

docker exec redis-lesson redis-cli SET temp "data"
docker exec redis-lesson redis-cli EXPIRE temp 100
docker exec redis-lesson redis-cli TTL temp
docker exec redis-lesson redis-cli PERSIST temp
docker exec redis-lesson redis-cli TTL temp
OK
1
99
1
-1

EXPIRE adds a TTL to a key that already exists — the 99 a moment later (not 100) is the same countdown-already-running behavior as before. PERSIST does the opposite: it strips the expiry entirely, and the key goes back to -1.

Atomic counters

docker exec redis-lesson redis-cli SET views 100
docker exec redis-lesson redis-cli INCR views
docker exec redis-lesson redis-cli INCRBY views 50
docker exec redis-lesson redis-cli GET views
OK
101
151
151

INCR and INCRBY happen atomically on the server — two clients incrementing the same counter concurrently can’t lose an update to a race condition the way a client-side read-modify-write (GET, add one, SET) could.

The bug: incrementing something that isn’t a number

docker exec redis-lesson redis-cli SET name "Dune"
docker exec redis-lesson redis-cli INCR name
OK
ERR value is not an integer or out of range

Same philosophy as the intro lesson’s WRONGTYPE error, applied to content instead of structure: INCR needs the string to actually parse as an integer, and refuses outright rather than guessing a starting value.

Practice

1. Use SET ... NX to implement a simple lock, and confirm a second attempt fails.
docker exec redis-lesson redis-cli SET lock:job1 "worker-1" NX
docker exec redis-lesson redis-cli SET lock:job1 "worker-2" NX
docker exec redis-lesson redis-cli GET lock:job1
OK

worker-1

The second SET ... NX returns nothing (not an error) because lock:job1 already exists — worker-2 never wrote anything, and worker-1’s value is still there. This is Redis’s simplest building block for a distributed lock.

2. Increment a value by a floating-point amount.
docker exec redis-lesson redis-cli SET price 9.99
docker exec redis-lesson redis-cli INCRBYFLOAT price 0.50
OK
10.49

INCR/INCRBY are integer-only; INCRBYFLOAT is the dedicated command for float arithmetic, still atomic on the server.

3. Set a key to expire, then remove the expiry before it fires, and confirm it survives.
docker exec redis-lesson redis-cli SET reprieve "still here" EX 3
docker exec redis-lesson redis-cli PERSIST reprieve
OK
1

PERSIST returns 1 when it actually removed an expiry (and 0 if the key had none to remove) — worth checking that return value in code that assumes the persist always “did something.”

Next: hashes, lists, and sets — the structures with more than one value per key.

Frequently Asked Questions

Why does TTL return -1 for some keys and -2 for others?
-1 means the key exists but has no expiry set — it will live forever unless something deletes it. -2 means the key doesn't exist at all, whether because it never did or because it already expired. Code that checks "is this about to expire" and treats both the same way will misbehave — -1 needs no action, -2 usually means the value has to be regenerated.
Is SET key value EX 5 the same as SET then EXPIRE?
Functionally similar, but SET ... EX 5 sets the value and the expiry in one atomic round trip. SET followed by a separate EXPIRE call is two operations — if a process crashes or another client reads the key between them, there's a brief window where the key exists with no expiry at all. Prefer the combined form whenever both are being set together.
Why did INCR fail on a key that clearly held a value?
INCR requires the string to actually parse as an integer — "Dune" doesn't, so Redis raises 'value is not an integer or out of range' rather than silently treating it as 0. This is the same enforce-the-real-type philosophy as the WRONGTYPE error from the intro lesson, just for content instead of structure.
What does SET key value NX actually guarantee?
NX makes the write conditional: it only succeeds if the key doesn't already exist, and the whole check-and-set happens as one atomic operation on the server. That atomicity is what makes it usable as a distributed lock — two clients racing to SET the same key with NX can't both succeed, unlike a check-then-set done as two separate round trips from the client.
Does INCR work on floating-point numbers?
No — INCR and INCRBY are integer-only. INCRBYFLOAT is the separate command for incrementing a value by a floating-point amount, and it exists specifically because INCR's atomicity guarantee doesn't automatically extend to float arithmetic without its own command.