When I first started working with Redis, I kept hearing "Redis is single-threaded" everywhere. But then I'd read about Redis 6.0 adding I/O threading, and I'd see mentions of background threads. I was confused. Is it single-threaded or not? Let me share what I learned after digging into this.
Related Resources:
Understanding Connections and Threads in Backend Services - Complete guide on threading models and event loops
Redis Interview Preparation Guide - Comprehensive Redis interview prep from basics to advanced
The Big Confusion: Is Redis Single-Threaded or Not?
Here's the thing - when people say "Redis is single-threaded," they're only telling part of the story. Let me break it down in simple terms:
What actually runs on a single thread:
- Executing commands (like SET, GET, LPUSH, etc.)
- The main event loop that processes requests
- Accessing and modifying data structures in memory
What uses multiple threads:
- Background I/O operations (writing to disk, closing files)
- Lazy memory freeing (since Redis 4.0)
- I/O socket operations (since Redis 6.0) - reading from and writing to network sockets
- Creating snapshots (RDB files) using fork()
So the answer is: Redis is "mostly single-threaded" for the important stuff (running commands), but it uses threads for everything else.
Why Keep Command Execution Single-Threaded?
This was the part that confused me the most. Why not just make everything multi-threaded and get better performance?
The creator of Redis, Antirez (Salvatore Sanfilippo), explained this really well. He initially didn't want to add threading at all. He said something like: "I/O threading is not going to happen in Redis because I think it's a lot of complexity without a good reason."
But here's why keeping command execution single-threaded makes sense:
Imagine you have a list in Redis. One thread is trying to add items to the front (LPUSH), while another thread is removing items from the back (RPOP). To make this safe, you'd need locks everywhere. Every operation would need to check if another thread is using the same data structure. That's a lot of complexity, and it can actually make things slower.
Instead, Redis says: "One thread handles all commands. No locks needed. Simple and fast."
Operations like hash rehashing (when Redis needs to reorganize data) and expiration (removing old keys) happen without any locking because only one thread touches the data. This eliminates entire categories of bugs and makes the code much simpler.
Common Misconceptions I Had (And You Probably Do Too)
Let me share the misconceptions I had, and what I learned:
"Redis is completely single-threaded"
Nope! Redis has been using background threads for slow disk operations since the early days. There's a subsystem called bio.c (Background I/O) that handles things like:
- Writing data to disk (fsync)
- Closing file descriptors
- Since Redis 4.0: lazy memory freeing (cleaning up memory in the background)
Redis 6.0 added I/O threads specifically for network operations. So Redis has been multi-threaded in some ways for a long time.
"Redis can't use multiple CPU cores"
This one surprised me. Redis 6.0's I/O threading can actually use multiple CPU cores for network operations. The benchmarks are impressive:
- Redis 8.0 shows 37% to 112% throughput improvement with io-threads set to 8
- AWS ElastiCache 7.1 can handle over 1 million requests per second per node
So Redis can definitely use multiple cores - just not for executing commands.
"Single-threaded means Redis is slow"
This is completely backwards! Single-threading actually makes Redis faster in many cases. Here's why:
No locking overhead: When you have multiple threads, you need locks to protect shared data. Even uncontended locks cost 100-1000 CPU cycles. Contended locks can cost 10,000+ cycles. Redis avoids all of this.
Better CPU cache usage: When data is in the CPU's L1 cache, it takes about 1 nanosecond to access. Main memory takes 60-100 nanoseconds. With a single thread, Redis keeps more data in the CPU cache because it's not constantly switching between threads.
No context switching: When the OS switches between threads, it has to save and restore state. This takes time. With a single thread, there's no context switching overhead.
For more on why single-threaded can outperform multi-threaded, check out: Understanding Connections and Threads in Backend Services
"Race conditions can't happen with Redis"
I wish this were true, but it's not. Race conditions absolutely can happen, just not in the way you might think.
Individual commands are atomic - a single SET or GET command will complete fully before another command runs. But if you do multiple commands in sequence, those sequences can interleave with commands from other clients.
For example:
- Client A does: GET balance, then SET balance 100
- Client B does: GET balance, then SET balance 200
These can interleave, causing race conditions. The official docs confirm this: "While this ensures atomicity for individual commands, race conditions can still manifest when multiple clients interact with Redis simultaneously."
"BLPOP blocks the server"
I thought blocking commands like BLPOP would freeze the entire Redis server. Not true! Blocking commands only block the specific client connection, not the server. Other clients can still send commands and get responses.
The Redis documentation is clear: "The interesting fact about blocking commands is that they do not block the whole server, but just the client calling them."
"I/O threading makes command execution parallel"
This was confusing for me. I/O threading in Redis 6.0+ handles:
- Reading from sockets
- Parsing the protocol
- Writing responses back
But all commands still execute one at a time on the main thread. The I/O threads just handle the network stuff, while the main thread does the actual work. This preserves atomicity - you still get the guarantee that commands execute completely before the next one starts.
"Transactions and pipelines work the same way"
They don't! Transactions are blocking - if you start a transaction and it takes a while, all other clients have to wait. Pipelines are non-blocking - commands from different clients can interleave even if they're in pipelines.
"Redis persistence is handled by the main thread"
Nope! When Redis creates snapshots (BGSAVE) or rewrites the AOF file (BGREWRITEAOF), it uses fork() to create a child process. The child process does all the heavy disk I/O work, while the main process keeps handling requests.
However, there's a catch: the fork() operation itself can cause latency spikes on large instances (over 10GB) because it needs to copy the memory page table. This is something to be aware of in production.
A Simple Analogy That Helped Me Understand
Think of Redis like a restaurant with one really efficient waiter:
The waiter doesn't serve one table completely before moving to the next. Instead:
Listens: Scans all tables to see who needs attention
Reacts: When a table raises their hand, takes their order quickly, sends it to the kitchen
Loops: While food is cooking, immediately moves to the next table that needs something
The waiter = Redis's event loop
Tables = client connections
Kitchen staff = OS kernel handling I/O
Raising hand = socket ready notification (via epoll/kqueue)
This is called I/O multiplexing. Instead of waiting at one table (blocking), the waiter checks all tables at once and only serves the ones that are ready. The epoll/kqueue system calls make this super efficient - they only return the sockets that are ready, making this O(1) instead of O(n).
For a complete explanation of event loops and I/O multiplexing, see: Understanding Connections and Threads in Backend Services
How Redis Actually Performs
The numbers are pretty impressive:
Single instance benchmarks:
- With pipelining (sending 16 commands at once): 1.5M+ SET operations per second, 1.8M+ GET operations per second on modest hardware
- Without pipelining: around 100,000-140,000 queries per second
- Redis 8.0 with I/O threads: Up to 7.4 million queries per second in benchmarks
Real-world production (Twitter's numbers):
- 105TB of RAM total
- 39 million queries per second across all instances
- 10,000+ Redis instances
- Timeline Service alone: ~40TB heap, ~30M queries per second, 6,000+ instances
- Latency through Redis: less than 0.5ms (compared to 10ms through their JVM/Finagle path)
- Each instance maintains 100K+ open connections without issues
AWS ElastiCache for Redis 7.1:
- 500+ million requests per second per cluster
- Microsecond response times
- 100% throughput improvement over Redis 7.0
- 50% reduction in P99 latency
The key insight here: CPU is not Redis's bottleneck. The official Redis documentation says: "Because the CPU is not Redis's bottleneck, it is most likely machine memory or network bandwidth. Since a single thread is easy to implement and the CPU will not become a bottleneck, it makes sense to adopt a single-threaded solution."
This aligns perfectly with the event loop model. For more on this, see: Understanding Connections and Threads in Backend Services
When single-threaded becomes a problem:
- CPU-bound operations like KEYS * (scanning all keys)
- Complex Lua scripts that take a long time
- SORT operations on large collections
- Deleting very large keys
- Fork() latency spikes on instances larger than 10GB
Gotchas I Wish I Knew Earlier
The KEYS Command Will Kill Your Performance
A LinkedIn engineer shared this story: "Apart from the 'DEL' command I had just used the previous night to delete 250k+ keys, ALL the slow commands shown by the SLOWLOG were the 'KEYS' command!"
The official Redis latency documentation warns: "A VERY common source of latency generated by the execution of slow commands is the use of the KEYS command in production environments."
Solution: Use SCAN instead. It's slower but doesn't block everything.
Lua Scripts Block Everything
When Redis runs a Lua script, it guarantees atomic execution. This means: "The script's execution blocks all server activities during its entire time."
A 3ms Lua script might not sound like much, but it can block thousands of commands. Keep your Lua scripts short and fast.
Fork() Can Cause Latency Spikes
When Redis creates a snapshot or rewrites the AOF file, it uses fork() to create a child process. The fork() operation itself needs to copy the memory page table. On large instances (over 10GB), this copying can take time and cause latency spikes.
Practical fixes:
- Use SCAN instead of KEYS
- Use UNLINK instead of DEL for large keys (UNLINK is non-blocking)
- Enable
lazyfree-lazy-eviction yesin your config - Keep Lua scripts short and simple
How to Configure I/O Threading (Redis 6.0+)
If you want to enable I/O threading, here's how:
io-threads 4 # Number of I/O threads (usually 2-4 is enough)
io-threads-do-reads yes # Enable threaded reads
You can also bind threads to specific CPU cores:
server_cpulist 0-7:2 # Bind main + I/O threads to cores 0-7, step 2
bio_cpulist 1,3 # Bind background threads to cores 1 and 3
The Main Takeaway
Here's what I learned: Redis's single-threaded command execution isn't a limitation you need to work around. It's a deliberate architectural choice that:
- Eliminates entire categories of bugs (no race conditions between threads)
- Achieves throughput that most applications will never need
- Keeps the code simple and maintainable
The evolution from purely single-threaded to I/O threading in Redis 6.0+ shows the Redis team threading around the core insight rather than abandoning it. They kept command execution single-threaded (the important part) and added threading only where it helps (network I/O).
This shows really good systems thinking - understanding what actually matters and optimizing only that.
Further Reading
If you want to dive deeper into related concepts:
Understanding Connections and Threads in Backend Services - Deep dive into threading models, event loops, and connection management
Redis Interview Preparation Guide - Practical Redis interview questions and implementation examples
I hope this helps clear up the confusion around Redis threading. It took me a while to understand it, but once I did, it all made sense. The key is understanding that "single-threaded" refers specifically to command execution, not the entire system.
SOCIAL SHARE CARD GENERATOR