Redis is often described as single-threaded, and this leads to a common assumption:
"If Redis is single-threaded, everything I do with Redis is automatically atomic."
That statement is only partially true.
A single Redis command executes without another Redis command interleaving with it. But once an operation involves multiple commands, application-side logic, or multiple clients, race conditions can still appear.
Understanding this distinction matters when Redis is used for:
- counters
- rate limiting
- distributed locks
- inventory
- booking systems
- idempotency
- caching
- coordination between workers
The key question is therefore not simply "is Redis atomic?" It's:
"Which sequence of operations is Redis executing atomically?"
1. What Does Atomic Mean?
Suppose we have:
counter = 10and two clients want to increment it. A naive implementation might do:
GET counter
counter = counter + 1
SET counterWith two clients:
Client A Client B
GET → 10
GET → 10
SET → 11
SET → 11The expected result was 12, but we ended with 11. This is a lost update.
The problem isn't that Redis executed two commands simultaneously. The problem is that the entire read-modify-write operation wasn't atomic.
2. Redis Commands Are Atomic
Now consider:
INCR counterINCR performs the entire increment as one Redis command:
Client A → INCR counter
Client B → INCR counter
Redis processes commands sequentially:
INCR → 11
INCR → 12There is no point where another Redis command can sneak into the middle of INCR. Therefore, INCR counter is atomic.
This is one of the most important Redis concepts: Redis command atomicity does not mean your application workflow is automatically atomic.
3. The Difference Between Atomic Commands and Atomic Workflows
Consider a booking system. You might want to:
- Check whether a technician is available.
- Assign the technician.
- Change the booking status.
You could write:
GET technician:123:status
SET technician:123:status busy
SET booking:456:status confirmedEach individual Redis command is atomic. But the workflow is not — another worker could execute between those commands:
Worker A Worker B
GET status → available
GET status → available
SET status → busy
SET status → busy
SET booking A confirmed
SET booking B confirmedNow two bookings believe they successfully acquired the same technician. This is where Redis atomicity becomes a distributed-systems problem rather than merely a Redis API problem.
4. SETNX: Atomic Check-and-Set
Redis provides primitives specifically designed for this type of coordination:
SET lock:technician:123 worker-A NX EX 30NX means: only set the key if it does not already exist. This combines "does the key exist?" and "create the key" into one atomic Redis operation.
Without NX, you might do:
EXISTS lock:technician:123
SET lock:technician:123 worker-Awhich creates a race. With NX, Redis performs the conditional operation atomically.
5. Why GET + SET Is Dangerous
This pattern appears everywhere:
const value = await redis.get("counter")
const next = Number(value) + 1
await redis.set("counter", next)It looks harmless, but the actual operation is GET, then an application-side calculation, then SET. Redis cannot make those three steps atomic just because each command is individually atomic.
If multiple Node.js processes execute this code:
Process A Process B
GET → 10
GET → 10
calculate 11
calculate 11
SET → 11
SET → 11The update is lost. Instead:
await redis.incr("counter")turns the entire state transition into one Redis command.
6. Atomicity With MULTI/EXEC
Redis also provides transactions:
MULTI
INCR user:123:requests
EXPIRE user:123:requests 60
EXECThe commands queued between MULTI and EXEC execute together without another Redis command being processed in the middle of that execution.
But there's an important distinction: Redis transactions are not the same thing as SQL transactions. MULTI/EXEC does not provide arbitrary rollback semantics. If a command executed during EXEC produces an application-level error, Redis doesn't restore the previous state like a traditional transactional database rollback.
Think of it more as:
MULTI → queue commands → EXEC → execute queued commands sequentiallyrather than:
BEGIN → execute → something failed → ROLLBACK everything7. WATCH: Optimistic Concurrency
Sometimes you need to perform a conditional update based on the current value, for example:
balance = 100
if balance >= 50:
balance = balance - 50A simple GET then SET has a race condition. Redis provides WATCH for optimistic concurrency control:
WATCH account:123:balance
GET account:123:balance
MULTI
SET account:123:balance 50
EXECIf another client modifies the watched key before EXEC, Redis aborts the transaction. The application can then read again, recalculate, and retry.
This is fundamentally different from a lock. You're saying "proceed only if the state I observed hasn't changed," rather than "nobody else is allowed to modify this state."
8. Lua Scripts: Make the Decision and Mutation One Operation
Sometimes Redis commands aren't enough. Imagine:
if stock > 0:
stock--
create reservationYou need read, check, and write to happen as one atomic unit. A Lua script can execute this logic inside Redis:
local stock = redis.call("GET", KEYS[1])
if tonumber(stock) > 0 then
redis.call("DECR", KEYS[1])
return 1
end
return 0From the client's perspective:
application → EVAL → Redis (read, check, decrement) → resultThe important part is that the logic runs inside Redis, rather than requiring multiple round trips between your application and Redis.
9. The Network Round Trip Is Part of the Problem
This is an important mental model. Consider:
const value = await redis.get(key)
const result = calculate(value)
await redis.set(key, result)The execution isn't simply GET → SET. It's more like:
Application → GET → Redis → response → Application
Application → calculation
Application → SET → RedisDuring that round trip, another client is free to modify the key. A Lua script changes the boundary:
Application → EVAL → Redis (GET, CHECK, SET) → ResponseThe critical state transition stays inside Redis.
10. Atomicity ≠ Isolation ≠ Durability
These concepts are easy to mix together.
- Atomicity — an operation happens as one indivisible unit (
INCR). - Isolation — concurrent operations don't observe problematic intermediate states.
- Durability — data survives failures according to the configured persistence guarantees.
Redis can provide atomic execution for commands and certain command groups, but that doesn't mean every Redis workflow automatically has all the transactional properties you might expect from a relational database. This distinction becomes especially important when Redis is being used as more than a cache.
11. A Mental Model for Designing Redis Operations
When designing a Redis operation, ask these questions in order.
Can this be expressed as one Redis command? If yes, prefer that: INCR, DECR, SET ... NX, HINCRBY, SADD, ZADD.
Do multiple commands need to execute together? Consider MULTI/EXEC.
Does the operation depend on a value that another client might change? Consider WATCH, or a Lua script.
Can the decision and mutation be performed entirely inside Redis? If yes, Lua scripting can often remove the race window.
12. The Most Important Lesson
The deepest Redis atomicity lesson isn't "Redis is single-threaded." It's this:
Atomicity is determined by the boundary of the operation.
INCR counter is one atomic operation. GET, calculate, SET is not. And GET, check condition, SET is not automatically atomic just because every individual Redis command is atomic.
When designing concurrent systems, identify the state transition first, then decide where that transition needs to become indivisible. That's the point where Redis stops being "just a cache" and starts becoming a coordination primitive for distributed systems.
Conclusion
Redis's single-threaded execution model guarantees that individual commands run atomically, but it says nothing about the workflows built on top of them. The moment a decision spans more than one command, one network round trip, or more than one client, the atomicity guarantee ends at the command boundary.
So the useful question when designing a Redis-backed system isn't "is Redis atomic?" It's "what exactly needs to happen atomically, and where does that boundary sit?" Once that's answered, the right tool tends to follow on its own: a single command like INCR or SET ... NX when the operation fits in one call, MULTI/EXEC when several commands need to execute together, WATCH when a decision depends on a value another client might change, and a Lua script when the read, check, and write all need to happen as one indivisible step inside Redis itself.
Treating Redis as a coordination primitive rather than just a cache starts with getting that boundary right.