Event Loop & IO Multiplexing: The Heart of Redis
Redis is legendary for its speed, often handling hundreds of thousands of requests per second on a single core. But how does a single-threaded application manage 10,000+ simultaneous TCP connections without breaking a sweat?
2. Internal Architecture & Components Involved
Components Involved
- File Descriptors (FDs): The OS handle for a network socket.
- I/O Multiplexor: The mechanism (
epoll,kqueue) that polls these FDs. - Event Handlers: Callback functions triggered when an FD is ready.
How Redis designs this feature
Redis uses a non-blocking architecture. Instead of "checking" a socket, it waits for the OS to send a signal that data is ready.
Trade-offs: The Single-Threaded Choice
- Pros: Total isolation. No race conditions. Zero overhead from lock contention or thread context switching.
- Cons: Any high-latency command (like
KEYS *) will block the entire server. You cannot "background" a slow user without specialized I/O threads.
1. The Intuition: The Busy Waiter Problem
Imagine a restaurant with a single waiter (the CPU thread).
- Blocking I/O (The Rookie Approach): The waiter takes an order, then stands at the kitchen window waiting for the chef to cook. While they wait, other customers are ignored. To serve 100 customers, you'd need 100 waiters. This is expensive and chaotic (context switching).
- Non-Blocking I/O with Multiplexing (The Redis Approach): The waiter takes an order, gives it to the kitchen, and immediately moves to the next table. They only return to a table when the "food is ready" signal goes off. One waiter can handle the entire room efficiently.
Real-world problem solved: Eliminating the overhead of multi-threading (locks, context switching, memory consumption) while maintaining massive concurrency.
2. Internal Implementation: The ae Library
Redis implement its event loop using a small library called ae (short for an event). It wraps platform-specific syscalls like epoll (Linux), kqueue (macOS), or select.
Data Flow & Architecture
Client → TCP → Event Loop → Command Queue → Parser → Execution → Data Store → Response
-
Registration: When a client connects, the socket (File Descriptor) is registered with the kernel.
-
The Wait: The process calls
epoll_wait(). This syscall is blocking, but it wakes up the moment any registered socket has data. -
The Dispatch: The kernel returns a list of "active" descriptors. Redis loops through this list and executes the requested logic (GET, SET, etc.).
-
Time Events: Before waiting again, Redis checks a "time events" list for scheduled tasks (like TTL expiration or background saves).
The Trade-off: The "Single-Threaded" Trap
Because every command runs in the same loop, an $O(N)$ command like KEYS * blocks everything. The waiter is stuck carving a giant ice sculpture at Table 5, and Table 1 can't even get their check.
3. Reimplementing in Node.js
Node.js is built on libuv, which is essentially a more complex version of Redis' ae. We can use the core net module to see this in action.
const net = require('net');
/**
* Our Minimal "Redis" Event Loop
*/
const server = net.createServer((socket) => {
// This is a "File Event" handler
socket.on('data', (buffer) => {
const raw = buffer.toString();
// Step 1: Simulated Protocol Parsing
const [command, ...args] = raw.trim().split(' ');
// Step 2: Sequential Execution (Atomic)
const response = handleCommand(command, args);
// Step 3: Non-blocking Write
socket.write(response);
});
});
// A "Time Event" - Every 100ms, perform maintenance
setInterval(() => {
// In real Redis, this would be 'activeExpireCycle'
performActiveExpiration();
}, 100);
server.listen(6379, () => {
console.log('Redis clone listening on port 6379');
});
function handleCommand(cmd, args) {
if (cmd.toUpperCase() === 'PING') return '+PONG\r\n';
if (cmd.toUpperCase() === 'SET') return '+OK\r\n';
return '-ERR unknown command\r\n';
}4. Performance, High Concurrency & Backpressure
High Concurrency Behavior
Redis handles 100k+ concurrent requests on a single core because it eliminates the CPU "stall" time caused by context switching. In a multi-threaded server, the CPU spends significant time managing thread stacks; Redis spends that time processing bytes.
Backpressure Handling
If a client sends data faster than Redis can parse it, the Query Buffer grows. To prevent memory exhaustion, Redis set a per-client limit. Once reached, Redis stops reading from that socket until the buffer is drained.
Bottlenecks & Scaling Limitations
- Bottlenecks: Single-core CPU speed and network interrupt handling. If the network card is saturated with interrupts, the CPU can't run the event loop fast enough.
- Scaling: Vertical scaling (faster CPU) helps, but horizontal scaling (Partitioning/Clustering) is the only way to scale beyond the limits of a single core.
5. Redis vs. Our Implementation: What we Simplified
- AE Library: Redis uses its own
ae.clibrary for raw event handling. We use Node'snetmodule, which is a high-level abstraction. - Zero-Copy: Redis attempts to use
read()directly into target buffers. Node.js often involves multiple buffer copies (Kernel -> V8 -> Application). - I/O Threads: Since Redis 6.0, networking (reading/writing bytes) can be offloaded to background threads, while the "Heart" remains single-threaded. Our Node implementation is strictly single-threaded for everything.
6. Why Redis is Optimized
Redis is optimized for CPU Cache Locality. By keeping the event loop compact and the data contiguous, it ensures that the data the CPU needs is almost always in the L1/L2 cache, avoiding the 100ns "trip" to RAM.
Q: If CPUs have 64 cores, why is Redis single-threaded? A: Because Redis is Memory-Bound, not CPU-Bound. The bottleneck is usually the memory bandwidth or the network. Multi-threading introduces locking overhead (mutexes) that can actually make Redis slower for simple operations.
Q: Does Redis use threads at all? A: Yes! Recent versions (6.0+) use threads for I/O threading (reading/writing to sockets) to offload the expensive syscalls from the main loop, but the execution of commands remains single-threaded.
6. Comparison with Real Redis
| Feature | Our Node Implementation | Real Redis (C) |
|---|---|---|
| Event Loop | libuv (complex, multi-thread pool) | ae (tiny, strictly single-threaded) |
| I/O | JavaScript Streams (some overhead) | Zero-copy syscalls (maximum efficiency) |
| Scheduling | setInterval / setImmediate | Binary min-heap for O(1) time events |
7. Summary & Key Takeaways
- Concurrency != Parallelism: Redis handles 10k clients concurrently via multiplexing, but executes commands sequentially.
- Atomicity: Being single-threaded is a feature—it means every operation is naturally atomic without locks.
- The Golden Rule: Never run O(N) commands in production.
Next Step in Evolution: Now that we can handle connections, how do we serialize data efficiently? Let's move to RESP: The Redis Serialization Protocol.