How Dragonfly Works Internally: Shared-Nothing Multi-Threading, Dash Hash Tables, and the Architecture That Outperforms Redis on a Single Node
A deep-dive into Dragonfly internals for senior engineers. Covers the io_uring event loop with shared-nothing thread-per-core architecture, the Dash hash table with lock-free concurrent access, mimalloc per-shard memory management, fork-free snapshot persistence via fiber serialization, multi-shard transaction coordination, and production tradeoffs vs Redis, Valkey, KeyDB, and Memcached.
Redis’s single-threaded architecture is elegant, but it has a ceiling. A single core on modern server hardware tops out somewhere around 100K-300K operations per second for typical workloads. If you need more throughput from one machine, you either shard with Redis Cluster (adding operational complexity and cross-slot constraints) or you look at alternatives that use all available cores.
Dragonfly takes the second path. It was built from scratch to use every core on a machine through a shared-nothing threading model, replacing Redis’s global lock-free single-thread with a system where each thread owns its slice of the keyspace and never contends with the others. The result is throughput that scales linearly with core count, with Redis and Memcached protocol compatibility preserved at the command layer.
This article walks through how Dragonfly actually works: the event loop, the core data structure, the memory allocator, persistence without fork, transaction handling across shards, and replication. Every section covers the internal mechanism rather than the marketing claim.
The Thread-Per-Core Event Loop with io_uring
Redis uses epoll (or kqueue on BSD) inside its ae event loop. Dragonfly uses io_uring, the Linux kernel interface introduced in 5.1 that allows I/O submissions and completions to be communicated through shared memory rings, avoiding system call overhead on the fast path.
The io_uring model matters here for a specific reason: Dragonfly runs one event loop per physical thread, and each loop needs to handle network I/O efficiently without blocking. With io_uring, a thread can submit read operations on multiple sockets and then process completions in a single pass without invoking epoll_wait at all on the hot path. Submissions and completions go through a pair of ring buffers shared between userspace and the kernel, reducing the per-operation overhead compared to epoll.
Each Dragonfly thread hosts a fiber scheduler built on top of the Boost.Fiber library. Within a single thread, multiple fibers multiplex cooperative execution over the thread’s event loop. A fiber that is waiting on a network read yields control back to the scheduler, which picks another runnable fiber. This gives Dragonfly concurrency within a shard without OS-level thread switches, and without the callback-inversion problem that plagues async code in most other languages.
Thread 0 (shard 0) Thread 1 (shard 1) Thread 2 (shard 2)
+------------------+ +------------------+ +------------------+
| io_uring loop | | io_uring loop | | io_uring loop |
| | | | | |
| fiber: conn A | | fiber: conn C | | fiber: conn E |
| fiber: conn B | | fiber: conn D | | fiber: conn F |
| fiber: snapshot | | | | |
+------------------+ +------------------+ +------------------+
owns keys {0..5461} owns keys {5462..10922} owns keys {10923..16383}
The key architectural constraint is that no thread touches another thread’s memory. Each shard owns a contiguous range of hash slots (the same 16,384-slot CRC16 keyspace Redis Cluster uses for familiarity). A command that affects keys on a single shard is handled entirely within one thread, with no locks and no inter-thread communication. This is what “shared-nothing” means in Dragonfly’s context: each thread is self-contained for the common case.
The dispatch layer determines which shard owns a key before the command executes. Single-key commands go directly to the owning shard’s fiber queue. Multi-key commands that span shards require the transaction subsystem, described later.
The Dash Hash Table
Redis uses a chained hash table (dict) with two arrays for incremental rehashing. The structure works well in a single-threaded context, but it cannot be accessed concurrently from multiple threads without external locking. Dragonfly replaces it with Dashtable (Dash hash table), a design that supports concurrent readers within a shard without locking, and enables the fiber-based snapshot mechanism.
Dashtable is based on a research paper on extendible hashing with concurrency support. The structure is organized into segments, where each segment is a fixed-size array of buckets. A global directory maps hash prefixes to segments. When a segment fills beyond its load threshold, it splits into two segments, and the directory is updated to point to both.
Global directory (array of segment pointers, indexed by top-K bits of hash):
[0b00] -> Segment A (keys whose hash starts with 00)
[0b01] -> Segment B (keys whose hash starts with 01)
[0b10] -> Segment C (keys whose hash starts with 10)
[0b11] -> Segment C (keys whose hash starts with 11, shared segment)
Segment layout:
+----------+----------+----------+----------+
| bucket 0 | bucket 1 | bucket 2 | ... |
+----------+----------+----------+----------+
Each bucket: fixed number of slots (e.g., 14), each slot holds key pointer + value pointer
The fixed-size bucket design is important for the snapshot mechanism. Because Dragonfly’s persistence does not use fork(), the snapshot process must read each bucket without conflicting with concurrent writes. Dashtable tracks a generation counter per bucket. The snapshot fiber reads each bucket, records the generation, and if the bucket is modified while being read (generation changes), it can detect the conflict and re-read. This is a form of optimistic concurrency control at the bucket level, applied specifically to the serialization path.
For normal read/write operations within a shard, there is no concurrent access at all: one fiber runs at a time per thread (fibers are cooperative, not preemptive). The concurrency properties of Dashtable matter for the snapshot path, where the snapshot fiber and request-handling fibers share the same thread and interleave execution.
The split operation in Dashtable is incremental, not stop-the-world. When a segment fills, it splits lazily as new keys are inserted. The directory doubles in size when all segments at a given depth are full, similar to extendible hashing. This avoids the global rehash pauses that a naive doubling hash table would cause.
Memory Layout and Cache Behavior
Each Dashtable bucket stores a small bitmap of hop information alongside the key-value slots, allowing SIMD-based probing across all slots in a single cache line comparison. Finding a key involves:
- Hash the key to get the segment index and bucket index.
- Load the bucket (fits in one or two cache lines).
- Use the SIMD comparison to find candidate slots.
- Compare the full key for each candidate (to resolve hash collisions).
This is faster than chained collision resolution because it avoids pointer chasing into separately-allocated linked list nodes. On a cache miss for the key data, the overhead is one extra cache miss per lookup rather than multiple.
Memory Management with Mimalloc
Redis uses jemalloc as its allocator. Dragonfly uses mimalloc, Microsoft’s general-purpose allocator that is designed for thread-local allocation with low fragmentation.
The more important property for Dragonfly’s design is that mimalloc integrates naturally with per-thread allocation heaps. Each Dragonfly thread has its own mimalloc heap. All allocations for keys and values owned by shard N happen through shard N’s heap. Because no other thread accesses shard N’s memory, there is never cross-thread contention on the allocator’s internal free lists.
This contrasts with a naive multi-threaded approach where a shared global allocator (like ptmalloc in glibc) would serialize on mutex-protected free lists. Under high allocation rates from many threads, the global lock becomes a bottleneck. Mimalloc’s thread-local heaps eliminate this bottleneck in Dragonfly’s workload because memory is allocated and freed on the same thread.
The per-shard heap also enables a clean metric for memory usage per shard, which feeds into Dragonfly’s maxmemory enforcement. Each shard tracks its own allocation total and applies eviction when its share of the global memory limit is exceeded, without needing cross-shard coordination for the eviction decision.
Redis and Memcached Protocol Compatibility
Dragonfly exposes two protocol endpoints simultaneously: RESP2/RESP3 (the Redis Serialization Protocol) and the Memcached text protocol. A single Dragonfly process can serve both Redis and Memcached clients at the same time on different ports.
The compatibility layer sits above the command dispatch layer. When a connection arrives on the Redis port, Dragonfly parses RESP frames. When a connection arrives on the Memcached port, it parses the ASCII text protocol (the binary Memcached protocol is also supported). Both parse paths produce an internal command representation that feeds into the same dispatch and execution engine.
// Connecting to Dragonfly with ioredis (standard Redis client)
import { Redis } from "ioredis";
const df = new Redis({ host: "localhost", port: 6379 });
// All standard Redis commands work
await df.set("session:abc", JSON.stringify({ userId: 42 }), "EX", 3600);
await df.zadd("leaderboard", 9850, "alice");
const top = await df.zrevrange("leaderboard", 0, 9, "WITHSCORES");
// RESP3 push types, client-side caching, attribute responses
// are also supported when the client negotiates RESP3
await df.call("HELLO", "3");
Command compatibility is not 100% across every Redis command and flag, but coverage is comprehensive for the commands that appear in production workloads: all string commands, hash commands, list, set, sorted set, key expiry, Lua scripting via EVAL, transactions with MULTI/EXEC, pub/sub, and streams. The pipelining semantics match Redis exactly.
One behavioral difference worth noting: because commands on the same key always land on the same shard thread, Dragonfly preserves per-key ordering guarantees. Pipelined commands to the same key execute in order. Cross-key ordering is subject to the multi-shard transaction protocol.
Snapshot Persistence Without fork()
The most architecturally interesting divergence from Redis is how Dragonfly handles persistence. Redis’s RDB snapshot uses fork(): the OS creates a copy-on-write child process that serializes the entire keyspace while the parent continues serving writes. The CoW mechanism means that any page the parent writes after the fork must be copied, potentially doubling memory usage during the snapshot window.
Dragonfly cannot use fork() straightforwardly because its shared-nothing model means there is no single consistent view of the keyspace in one process address space at a snapshot-start instant. Instead, Dragonfly implements fiber-based incremental serialization.
When a snapshot is triggered, each shard starts a snapshot fiber. This fiber iterates through the shard’s Dashtable, serializing each bucket to the snapshot output. Because the snapshot fiber runs within the same thread as the request-handling fibers, it interleaves with live writes to the shard. The Dashtable generation counters and a per-key dirty tracking mechanism allow the snapshot fiber to detect which keys have been modified since the snapshot started.
The protocol is:
- At snapshot start, each shard records a logical start timestamp.
- The snapshot fiber reads each bucket. If the bucket’s generation counter has advanced since snapshot start, the key was modified after the snapshot began. The pre-modification value (captured in a side buffer at write time) is serialized instead.
- If no side buffer exists (the key was new after snapshot start), it is excluded from this snapshot.
Shard thread timeline:
t=0 [snapshot start: record generation baseline]
t=1 [snapshot fiber: serialize bucket 0, gen=5]
t=2 [write fiber: update key in bucket 0, gen becomes 6, save old value to side buffer]
t=3 [snapshot fiber: serialize bucket 1, gen=3]
t=4 [snapshot fiber: bucket 0 was already serialized at t=1, skip]
...
t=N [snapshot fiber: all buckets done, flush output]
The result is a point-in-time consistent snapshot that corresponds to the state at t=0, regardless of how many writes happened during serialization. Memory overhead during a snapshot is bounded by the side buffer for modified keys, not by CoW-duplicated OS pages. On write-heavy workloads, this is substantially less memory than the Redis fork approach.
Dragonfly writes snapshots in an RDB-compatible format, so existing tooling for Redis backups (rdbtools, redis-rdb-tools, RedisInsight import) works with Dragonfly snapshots.
Multi-Shard Transactions
Single-key commands are trivial: dispatch to the owning shard, execute, return. Multi-key commands (MGET, MSET, SUNIONSTORE, Lua scripts touching multiple keys) require coordinating across shards without deadlocks and without global locking.
Dragonfly uses a two-phase locking protocol on top of its fiber scheduler:
Phase 1: Lock acquisition (hop phase)
The transaction coordinator (running in the initiating connection’s fiber) sends a lock request to each involved shard. Each shard queues the transaction in its local transaction queue. A transaction does not proceed until all involved shards have acknowledged the lock.
Phase 2: Execute
Once all shards have acknowledged, the transaction executes on each shard. Because all shards have reserved their local lock, no other transaction can modify the involved keys until this transaction completes. Execution happens in parallel across the shard threads.
Commit and unlock
After execution, each shard sends an acknowledgment back to the coordinator. The coordinator collects all results, assembles the response, and releases the locks.
Connection fiber (coordinator):
1. Identify shards: key "a" -> shard 0, key "b" -> shard 2
2. Send LOCK(txn_id, [key_a]) to shard 0
3. Send LOCK(txn_id, [key_b]) to shard 2
4. Wait for both ACK_LOCK
5. Send EXEC(txn_id) to shard 0, shard 2 (parallel)
6. Wait for both EXEC_RESULT
7. Assemble response, release locks
The two-phase protocol introduces latency proportional to the number of shards involved: two round-trips of inter-thread messaging per multi-key operation. For single-key commands, which represent the vast majority of real workloads, there is no cross-shard overhead at all.
Dragonfly optimizes the common case of MULTI/EXEC blocks: when the transaction block is analyzed and all keys are known upfront (no WATCH-conditional logic), Dragonfly can pre-declare the involved shards and avoid a second planning phase. WATCH adds complexity because the watched key set may cause a transaction to abort, requiring the coordinator to release partial locks.
Replication
Dragonfly supports primary-replica replication with a protocol that is wire-compatible with Redis replication at the high level (PSYNC, replication backlog, partial resync) but differs internally because the primary has multiple shards.
Each shard on the primary maintains its own replication journal, recording the stream of mutations for keys in that shard. On the replica side, each shard has a corresponding consumer that applies the journal entries. Replication is therefore sharded on both the primary and replica, parallelizing both the write-ahead journal production and the apply phase.
The initial full sync sends a snapshot (using the fiber-based serialization described above) rather than a forked RDB child. This means the full sync does not spike memory on the primary, which is relevant when syncing large replicas.
Partial resync after a replica reconnect works analogously to Redis: the replica sends its replication offset per shard, and the primary checks whether the requested offset is within the in-memory journal window. If yes, it streams the delta. If the journal window has been overwritten, a full sync is required.
Sentinel-style failover and Dragonfly Cluster mode are available, though as of mid-2026 the cluster topology management tooling is less mature than Redis Cluster’s.
Production Considerations
CPU pinning and NUMA
Dragonfly’s throughput scales with core count, but only if threads are not migrated by the OS scheduler. Pin Dragonfly threads to specific CPUs using taskset or the --cpuset flag. On NUMA machines, pin threads to cores within the same NUMA node as the network interface to avoid cross-NUMA memory access on the I/O path.
# Pin to cores 0-7 on a multi-core server
taskset -c 0-7 dragonfly --port 6379 --maxmemory 16gb
Thread count and maxmemory
Dragonfly defaults to using all available cores. For a shared host, cap the thread count with --threads N. Each shard gets approximately maxmemory / N before it begins eviction, so uneven key distributions can cause one shard to evict while others are underutilized. Uniform key distribution (avoid using a single key as a hotspot) matters more in Dragonfly than in Redis.
Hot key problem
A single hot key in Redis harms one core. A single hot key in Dragonfly harms one shard thread, which is the same problem. Dragonfly does not solve the hot key problem: if 90% of your traffic is reads on a single key, that shard’s thread becomes the bottleneck regardless of how many cores the machine has. Mitigation options are the same as Redis: client-side caching, read replicas, or key sharding at the application layer.
Snapshot frequency and side buffer memory
Dragonfly’s snapshot mechanism avoids CoW memory doubling, but the side buffers for modified keys do consume memory during a long-running snapshot. On a write-heavy shard with a snapshot that takes several minutes, the side buffer can grow proportionally to the write rate times the snapshot duration. Monitor dragonfly_snapshot_serialization_bytes and dragonfly_snapshot_duration_seconds to understand your actual overhead.
RESP3 and client-side caching
Dragonfly supports the RESP3 protocol and client-side caching via the CLIENT TRACKING command, identical to Redis 7. This allows clients to cache key values locally and receive invalidation messages when the key changes, eliminating round-trips for frequently read, rarely written keys. Not all client libraries expose this feature; verify your client’s RESP3 support before building around it.
// Enable server-assisted client-side caching (requires RESP3)
const df = new Redis({ host: "localhost", port: 6379 });
await df.call("HELLO", "3");
await df.call("CLIENT", "TRACKING", "ON", "BCAST", "PREFIX", "user:");
// The client now receives invalidation notifications for keys prefixed "user:"
// via the push message channel, without needing to track them individually
Tradeoffs Comparison
| Dimension | Dragonfly | Redis | Valkey | KeyDB | Memcached |
|---|---|---|---|---|---|
| Threading model | Shared-nothing, one fiber scheduler per core | Single-threaded command exec, optional I/O threads (v6+) | Single-threaded (Redis 7.2 fork) | Multi-threaded with lock striping | Multi-threaded, partitioned per core |
| Hash table | Dashtable (extendible hashing, segment-based, snapshot-friendly) | Dict (chained, incremental rehash across two arrays) | Dict (same as Redis) | Dict (same as Redis) | Slab-based, no hash table for the data store |
| Memory allocator | mimalloc with per-shard heaps | jemalloc with thread caches | jemalloc | jemalloc | Slab allocator (built-in) |
| Snapshot persistence | Fiber-based incremental serialization, no fork, bounded side-buffer overhead | RDB via fork + CoW, AOF append-only, hybrid mode | RDB + AOF (Redis-compatible) | RDB + AOF (Redis-compatible) | None |
| Throughput ceiling | Scales with core count; 4M+ ops/sec on 64-core hardware reported | ~300K ops/sec single core with I/O threading | Similar to Redis 7.x | 2-4x Redis on multi-core via threading | ~1M ops/sec per instance, strings only |
| Multi-key transactions | Two-phase locking across shards, parallel execution | Serial within single thread, MULTI/EXEC blocks | Serial (Redis-compatible) | Serial per-thread with cross-thread coordination | No transactions |
| Protocol | RESP2, RESP3, Memcached text + binary | RESP2, RESP3 | RESP2, RESP3 | RESP2, RESP3 | Memcached text + binary |
| Data structures | Full Redis type set (strings, hashes, lists, sets, sorted sets, streams, HyperLogLog, geo) | Full type set | Full type set | Full type set | Strings only |
| Operational maturity | Production since 2022, growing adoption, active development | 15+ years, extensive ecosystem | High, Linux Foundation backed (2024 fork) | Moderate, production-ready since 2019 | Very high, decades of production use |
| Sweet spot | Max single-node throughput with Redis compatibility, avoiding Redis Cluster for throughput reasons | General-purpose, session cache, pub/sub, rate limiting, leaderboards | Drop-in Redis replacement under open license | Active-active replication, multi-master setup | Pure ephemeral string cache, simplicity above all |
The architectural gap between Dragonfly and Redis is most visible at high core counts under concurrent load: Redis’s single execution thread becomes the constraint, while Dragonfly’s throughput continues to scale. The gap narrows on workloads dominated by a single hot key (one shard owns it and handles all traffic for it) or on workloads that require complex multi-key transactions (cross-shard coordination adds latency).
Closing
Dragonfly’s design is a coherent response to a specific constraint: extracting Redis-compatible performance from modern multi-core hardware without the operational overhead of a sharded cluster. The shared-nothing threading model, Dashtable’s segment structure, mimalloc’s per-shard heaps, and the fork-free snapshot mechanism each solve a real problem that Redis’s architecture pushes onto the operator. Whether those tradeoffs are the right ones for a given system depends on the workload, the tolerance for a less mature operational ecosystem, and whether the throughput gains justify moving away from Redis’s battle-tested tooling.
More in System Design
How Amazon Aurora Works Internally: The Log-Is-the-Database Architecture, Quorum Writes, and Storage-Compute Separation Behind Cloud-Native SQL
A deep dive into Aurora's storage-compute separation, the log-is-the-database design that pushes redo log processing to storage nodes, quorum-based replication across 6 copies in 3 AZs, protection group architecture, fast cloning, Serverless v2 scaling mechanics, and honest tradeoffs versus RDS, self-managed PostgreSQL, CockroachDB, and Cloud Spanner.
How CockroachDB Works Internally: Distributed SQL, Raft Consensus Per Range, and the Architecture Behind Serializable Transactions at Global Scale
A deep dive into CockroachDB's internals: the 512MB range-based data model with automatic splitting, per-range Raft replication, MVCC timestamp ordering with hybrid logical clocks, DistSQL physical planning, serializable transactions with timestamp refreshes, closed timestamps for follower reads, online schema changes using multi-version schema descriptors, and production considerations for hotspots and write amplification.
How Container Runtimes Work Internally: Namespaces, Cgroups, and the OCI Stack From Docker Run to Process Isolation
Containers are not virtual machines and they are not magic. They are a thin composition of Linux kernel primitives: namespaces, cgroups, and a layered filesystem. This article traces exactly what happens between docker run and a running process, covering the OCI spec, containerd, runc, overlay filesystems, and container networking at the kernel level.
How YugabyteDB Works Internally: DocDB Storage, Tablet Splitting, and the Dual API Architecture That Scales PostgreSQL Horizontally
A deep dive into YugabyteDB internals covering the DocDB storage engine built on RocksDB LSM trees, per-tablet Raft consensus, YSQL and YCQL dual query layers, distributed MVCC with hybrid logical clocks, automatic tablet splitting and rebalancing, xCluster multi-region replication, and production considerations for hotspots, connection pooling, and schema design.