March 21, 2023·6 min read·engineeringdragonflyarchitecturescaling

Dragonfly 1.0 makes vertical scaling its deployment model

Dragonfly reaches general availability with replication and high availability. Its internal shards and dictionary design explain the case for a single larger server and the limits of Redis compatibility.

Dragonfly's general-availability announcement starts with an operational problem. A growing cache can require several server processes before the underlying machine has exhausted its useful capacity. More processes introduce placement and management decisions that application developers would prefer to avoid. The March 21 announcement presents Dragonfly 1.0 as an alternative built around using a larger instance through one Redis-compatible endpoint.

The company also announces $21 million raised from Redpoint and Quiet Capital. That funding accompanies a specific product commitment. Version 1.0 includes snapshotting, replication, and high availability alongside common Redis data types and commands. The architectural argument concerns how the server uses hardware, while the release milestone concerns whether the surrounding datastore functions are ready for production use.

I find that distinction useful because the reason I reach for Dragonfly is operational simplicity when one Redis process becomes limiting. A higher throughput number alone does not explain that preference. The more interesting question is whether a single endpoint can exploit the resources already available in a machine without requiring the application to manage the partitioning itself.

Internal shards distribute ownership

The 1.0 README describes a shared-nothing design that partitions the keyspace among worker threads. Each thread manages its own dictionary slice, which Dragonfly calls a shard. The shards discussed here are internal to one process. Their existence does not imply that an operator has deployed a distributed cluster.

Ownership matters because adding threads to a database does not automatically make independent operations run efficiently. Shared mutable structures can require coordination between workers. Partitioning the dictionary gives each worker a defined set of records to manage, so operations involving unrelated keys can use different workers. Operations involving several keys still need a coordination mechanism.

Dragonfly bases its transaction framework on the VLL lock-manager research identified in the README. The project describes composing atomic multi-key operations without mutexes or spinlocks. The relevant point is that internal partitioning and multi-key atomicity must coexist. Redis clients can issue operations whose meaning includes more than one key, so redistributing ownership cannot silently remove that contract.

This also explains why scaling one process is an implementation choice with application consequences. A client should not have to discover which worker owns a key. The server accepts the protocol and coordinates the internal work. A deployment gains an opportunity to use more cores without presenting the internal thread layout as another topology for application code to manage.

The model still has a machine boundary. Internal shards distribute work within a server, and replication creates another copy elsewhere. Those mechanisms serve different purposes. More worker threads do not create an independent failure domain, while a replica does not explain how the primary uses its own CPU cores.

Dictionary growth affects capacity

The release-tagged dictionary design document describes an adaptation of extendible hashing. A directory points to fixed-size segments. An insertion selects a segment using the key's hash, then attempts to place the record into that segment. When the relevant candidate buckets cannot accept it, the table splits the segment.

That is a more local growth operation than allocating another full-size bucket array for the entire dictionary. The document explains that the split touches the old segment and its new partner. Other segments do not need to participate in that particular redistribution. Incremental growth therefore follows the parts of the table that need capacity.

The concrete layout described in the document has fifty-six regular buckets and four stash buckets per segment. Each bucket has fourteen slots. A record first tries its home bucket, then a neighbouring bucket, and then the stash. A segment can require a split even when some unrelated bucket remains empty because an individual record has a bounded set of candidate locations.

This is useful detail because the phrase memory efficient can conceal several unrelated effects. The directory uses one pointer per segment, while the segment stores multiple records. Open addressing avoids a linked-list allocation for every dictionary entry. Local splits change the temporary allocation pattern during growth. These mechanisms reduce dictionary-management overhead without changing the application's payload size.

The document explicitly distinguishes dictionary savings from total memory consumption. A cache full of substantial values still spends much of its memory on those values. A cache of tiny keys and values exposes metadata costs more directly. Consequently, a percentage improvement measured for one dataset is not a general formula for the memory required by another.

Snapshotting belongs in the memory model

A datastore needs a consistent saved view while applications continue to modify data. Dragonfly's dictionary document describes an asynchronous snapshot algorithm with point-in-time guarantees. It also states that SAVE and BGSAVE use the same procedure in Dragonfly. The familiar command names therefore sit above a different implementation from one based on creating a child process.

The design uses dictionary metadata to support forkless saving. This connects a data-structure choice to an operational task that otherwise looks separate from ordinary lookup. The same table that controls growth also supplies information used to preserve the snapshot's view while records change.

The document includes an experiment that loads data, applies update traffic, and starts snapshotting. It measures memory through cgroups so that a forked process and its parent are counted together. That measurement choice matters to the comparison. Looking only at the primary process's reported allocation would answer a different question from counting the memory used by the full snapshot operation.

Those results are project measurements under the stated workload. Their architectural explanation is more reusable than their particular ratio. Snapshot memory depends on how the implementation preserves the old view during writes. The forkless approach changes that mechanism, and the dictionary document explains why its metadata is relevant.

I regard predictable memory during persistence work as part of the argument for a larger single instance. Capacity planning has to include work that overlaps application traffic. A server that fits its live dataset but needs substantially more memory for an ordinary management operation requires a different instance size from one whose transient requirements are smaller.

Compatibility includes deliberate differences

The announcement describes a drop-in Redis replacement, but the versioned differences document is more specific about boundaries. It limits strings to 256 MB and documents constraints on string indices. It also explains expiry rounding for sufficiently distant millisecond deadlines.

For PEXPIRE and PSETEX, the document says deadlines beyond 2^28 milliseconds round to the nearest second. Applications whose correctness depends on distant deadlines retaining millisecond precision therefore have a concrete semantic difference to account for. The existence of the command does not imply identical representation of every argument.

Lua is another example. The document identifies Lua 5.4.4 and support for Lua integers. That version choice is relevant to scripts, even if the wire command used to invoke a script remains familiar. Protocol compatibility and language-runtime behaviour are different layers of the same application interaction.

The following configuration uses options documented in the 1.0 README and makes a local-only endpoint with an explicit memory limit:

./dragonfly --bind=localhost --port=6379 --maxmemory=12gb

It illustrates the single-server model without implying that these flags configure replicas or a failover monitor. The README separately documents snapshot scheduling and cache mode. Choosing whether to evict near the memory limit is a datastore policy, while choosing how much memory to allow is a resource decision.

General availability changes the reliability claim

The announcement's inclusion of replication and high availability is significant because vertical scaling and failure handling solve different problems. A larger machine can host more data and more work, but losing that machine still removes the endpoint. Replication and promotion are necessary parts of a design that intends to keep serving after that loss.

The company reports throughput and memory comparisons in the launch post. Their practical meaning remains tied to the implementation and workloads behind them. The shared-nothing design explains why several cores can contribute to request processing. The dictionary document explains where metadata and snapshot savings originate. These are independently understandable mechanisms even when an application's workload differs from the published benchmark.

The announced roadmap includes further SSD work and a cloud offering. As of this launch, those are future plans described by the company. They should not be counted as capabilities already delivered by the general-availability milestone. The immediate release claim concerns the datastore and its replication, persistence, and availability functions.

Dragonfly 1.0 gives the vertical-scaling approach a broader operational basis. Internal dictionary shards supply parallel ownership, while local dictionary growth and forkless snapshots change memory requirements during normal operation. The documented differences remain relevant for applications that rely on large strings, long expiration intervals, or Lua behaviour.