August 15, 2023·6 min read·engineeringvalkeyredismemoryreleases

Redis 7.2 reduces overhead for small collections

Redis 7.2 reaches general availability with compact representations for small sets and lists. The implementation ties memory savings to collection shape and conversion thresholds.

A large number of small collections can spend substantial memory on their representation rather than their contents. Redis 7.2 addresses that overhead by extending compact encodings for sets and lists. The application can keep using the same commands while the server chooses a different internal structure for collections that fit its configured limits.

The 7.2 release notes identify August 15 as the first stable release of the branch. The notes pinned to 7.2.0 collect the changes developed through its release candidates, including memory optimizations for small collections. Those changes are worth understanding through their representation choices, since memory savings depend on what an application actually stores.

I have used Redis since its early releases, and its collection commands are a large part of why I continue to value it. Reducing overhead while preserving that command model is useful. The tradeoff is that logical type and physical representation no longer tell the same story about the cost of an operation.

Small sets gain another compact representation

Redis already has a special integer-set representation for sets composed of strings that can be represented as signed 64-bit integers. The 7.2 configuration file retains set-max-intset-entries, with a default of 512. Version 7.2 also supplies listpack limits for sets containing non-integer values.

The new defaults allow up to 128 entries and set a maximum value length of 64. These are representation thresholds, not limits on the logical set size an application may store. When a collection needs a representation outside those bounds, the server can use a hash table instead.

The following excerpt shows the version's configuration names and defaults. It describes storage selection rather than a new application data type.

set-max-intset-entries 512
set-max-listpack-entries 128
set-max-listpack-value 64

The set implementation chooses an integer set when the initial value and incoming size hint permit it. If a listpack is suitable, it chooses that representation. Larger initial operations can create a hash-table-backed set and expand its dictionary using the hint.

That initial choice does not mean a set must retain one representation forever. The code contains conversion paths when the existing compact form no longer fits the incoming operation or stored data. The application still sees set semantics, including whether a member was newly added or already existed.

Compact storage changes lookup work

In the listpack insertion path, the code locates the first entry and calls lpFind() to check whether the proposed member already exists. In the hash-table path, insertion uses the dictionary's lookup and insertion machinery. These are different implementation costs beneath the same SADD operation.

This is why the thresholds matter. Compact storage can reduce metadata overhead for a bounded collection, while searching through that compact representation is a different operation from a dictionary lookup. Keeping the representation limited to small collections makes that tradeoff explicit in the configuration.

The release's set-encoding pull request also changes operations that iterate or persist sets. The AOF rewrite path uses the generic set iterator, allowing it to emit logical set members without assuming only integer-set and hash-table encodings exist. Defragmentation gains handling for the compact representation as well.

A representation change therefore reaches beyond the command that creates an object. Persistence, scanning, and memory management must understand it too. That integration is part of what makes the optimization usable through the existing database interface instead of exposing a separate specialized collection to applications.

Input size hints can trade capacity for less repeated work

The final set factory accepts a size hint for the operation's expected additions. A large multi-member request can select a hash table immediately, even before every argument has been processed. Existing compact sets can also convert based on that hint.

The source comments identify the limitation. A request with many duplicate members may cause the initial capacity to exceed the number of distinct members actually stored. The implementation accepts that possible over-allocation to avoid unnecessary intermediate work while inserting a large batch.

That gives collection shape two meanings. Final cardinality affects the useful retained data, while the shape of the incoming command can affect how the server prepares storage. An optimization based on argument count necessarily estimates how much new information those arguments contain.

The relevant benefit is therefore specific. It reduces avoidable representation and capacity changes for sufficiently large insertions. It does not establish a fixed percentage improvement for every small set, nor does it imply that duplicate-heavy input always achieves the smallest possible allocation.

Small lists can omit a quicklist wrapper

The list implementation supports a listpack directly for a sufficiently small list. When it grows beyond the applicable size or count limit, the server creates a quicklist and appends the existing listpack as a packed node. That avoids rebuilding the logical list element by element merely to introduce the larger representation.

The list optimization change updates persistence and memory-management paths to handle both forms. AOF rewriting iterates through the logical list interface. Defragmentation distinguishes a direct listpack from a quicklist. These changes preserve the operation's meaning while allowing a smaller object layout.

The choice is useful for workloads with many short lists because a wrapper's overhead repeats for every key. An application need not change its push and pop commands to benefit. The server owns the representation decision and the transition to a structure suited to larger lists.

The implementation also supports conversion in the other direction when a list becomes small enough. It requires a quicklist with a single packed node before extracting that node's listpack and releasing the wrapper. A multi-node quicklist does not meet that direct conversion condition.

Shrinking uses a different threshold

Repeated insertion and removal near one boundary can cause repeated representation changes if both directions use the same cutoff. The list conversion code avoids that behavior by lowering the threshold when considering a shrinking list. It halves the size and count limits before deciding whether to convert the single-node quicklist back to a direct listpack.

This creates a deliberate gap between growing out of the compact form and shrinking back into it. An object can remain in the larger representation for a while after it stops growing. That retained wrapper is a tradeoff against repeatedly converting an object whose size oscillates around the boundary.

The code comments name frequent insertion and deletion as the reason. This is a concrete operational compromise, rather than evidence that the representation selector always chooses the smallest structure after every mutation. Avoiding unnecessary conversion work is itself a resource decision.

I prefer this explicit treatment of the boundary. A workload's memory cost includes allocation churn as well as the bytes retained at one instant. The different shrinking threshold gives that churn a place in the implementation's policy.

Stable release also establishes compatibility boundaries

The release notes specify RDB format version 11 and warn that older Redis versions cannot read it. Compact internal representations therefore sit within a release that also changes saved-file compatibility. An application command remaining familiar does not make the resulting persistence files universally readable by older servers.

The branch also includes WAITAOF, which waits for local or replica AOF synchronization for preceding writes on the current connection. That is a separate durability-related addition. It should not be confused with the collection encodings, which concern how the server represents and processes data in memory.

Operational changes include event-loop timing metrics and client-library identity fields. Those supply ways to inspect execution and identify connections, but they do not convert a memory optimization into a measured throughput guarantee for a particular deployment. The release notes identify several improvements without giving one universal workload result.

Logical commands stay stable while storage adapts

The memory work in 7.2 has a precise scope. Non-integer sets can use a bounded compact representation, short lists can avoid a quicklist wrapper, and conversion logic handles growth and shrinkage. Persistence and other object-management paths use interfaces that accommodate those representations.

For applications, the useful consequence is that ordinary collection commands can retain their meaning while the server spends less metadata on suitable objects. The configured thresholds, incoming batch shape, and shrinking policy determine when that consequence applies. Those are concrete properties to examine when a dataset contains many small collections.