The idea: one type, several layouts

A Redis key holds a string, a hash, a set, a sorted set or a list. That is the value's type, and it decides which commands work on it. How the bytes are laid out in memory is a separate choice, the encoding. Redis keeps small values in compact, pointer-free blocks and switches to bigger general structures only when a value grows. OBJECT ENCODING key tells you which one a key uses.

The 16-byte redisObject with type, encoding, lru, refcount and ptr. Below it three strings: int, where ptr holds the number 42; embstr, where the robj and the SDS header len, alloc, flags, the bytes hello and a trailing zero sit in one 25-byte allocation; raw, where ptr points to a separate SDS with spare room after APPEND
Every value starts with the same 16-byte header; what ptr points to depends on the encoding.

The page runs real commands against a few keys and draws, after each step, the key's header and the bytes behind it. Pick a command and press Run, or run a demo. The thresholds are scaled down (4 entries and 8 bytes instead of 128 and 64) so a conversion happens after a few clicks; the CONFIG box on the canvas lists the real defaults.

The redisObject

Every value in the keyspace is a redisObject (robj) of 16 bytes:

FieldSizeWhat it holds
type4 bitsOBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET, OBJ_HASH (and streams, modules)
encoding4 bitsOBJ_ENCODING_INT, EMBSTR, RAW, LISTPACK, INTSET, HT, SKIPLIST, QUICKLIST
lru24 bitsthe LRU clock when the key was last used, or LFU counter data (see expiry and eviction)
refcount4 byteshow many places point at it; shared objects use INT_MAX
ptr8 bytesthe address of the data, or for the int encoding the number itself

On top of that, each key costs a dictEntry (24 bytes) in the keyspace's hash table, a bucket pointer and an SDS string for the key name. The ≈ bytes on the canvas add these up.

Strings: int, embstr, raw

When a string is stored, tryObjectEncoding picks the smallest form (Demo: string int → embstr → raw):

  • int: the value is a whole number that fits a C long, like 42 or 123456. The number goes into ptr itself, so there is no second allocation. Numbers 0 to 9999 go further: Redis keeps one shared object for each (shared.integers, OBJ_SHARED_INTEGERS = 10000), and every key with that value points at it.
  • embstr: up to 44 bytes. The robj and the string are one allocation: 16 bytes of robj + 3 bytes of SDS header + 44 bytes + the trailing \0 = 64 bytes, exactly one jemalloc size class. One malloc, one free, and the bytes are in the same cache line as the header.
  • raw: longer strings, or any string that has been changed. The robj and the SDS are two allocations.

An SDS (simple dynamic string) is a small header followed by the bytes: len (bytes used), alloc (bytes reserved), flags (which header size: SDS_TYPE_8 uses 1-byte len and alloc for strings under 256 bytes), then buf and a trailing \0, so C functions can read it too. ptr points at buf; the header sits just before it.

An embstr is read-only. APPEND first copies an int or embstr value into a new raw SDS of exactly the right size, then sdsMakeRoomFor grows it to twice the new length (for strings under 1 MB; above that, by 1 MB at a time). The next APPEND fits into the spare room without a realloc. SETRANGE, INCRBYFLOAT and friends work the same way.

A listpack, byte by byte

A small hash, set, sorted set or list is one listpack: a single block of bytes with no pointers inside (Demo: hash listpack → hashtable).

The listpack of a hash with the fields name = ann and age = 31: a 4-byte total of 25, a 2-byte count of 4, then four entries. name is a 6-bit string: encoding byte, 4 data bytes, backlen 5. ann: encoding, 3 bytes, backlen 4. age: encoding, 3 bytes, backlen 4. 31 is a 7-bit integer: one byte, backlen 1. The block ends with the byte 0xFF
A listpack is a header, entries of encoding + data + backlen, and an end byte: a hash with two fields fits in 25 bytes.
  • Header: the total size in bytes (4 bytes) and the number of entries (2 bytes).
  • Entries: an encoding byte that says what follows, the data, and backlen, the entry's own length, so the listpack can also be walked from the end. Strings up to 63 bytes take 1 byte of encoding (str6). Integers are stored as integers: 0 to 127 in a single byte (int7), then 13-bit, 16, 24, 32 and 64-bit forms.
  • End byte: 0xFF.

A hash stores field, value, field, value. A sorted set stores member, score, kept in score order. Finding something means walking the entries, which is fast while the listpack is short and stays in a few cache lines. That is why there is a size limit. (The listpack replaced the older ziplist in Redis 7; the ziplist had a "previous entry length" field that could set off cascading updates.)

The intset

A set of only integers starts as an intset: a sorted array of fixed-width numbers after an 8-byte header (encoding, length). Lookups are a binary search. All elements have the same width, int16, int32 or int64. When a number arrives that doesn't fit, like 70000 in an int16 set, intsetUpgradeAndAdd widens every element, working from the back so nothing is overwritten (Demo: set intset → listpack → hashtable). An intset never narrows again.

From small to big

Four rows. Hash: listpack to hashtable when more than hash-max-listpack-entries 128 fields or a field or value longer than 64 bytes. Set: intset to listpack when a non-integer arrives and the set is small, intset or listpack to hashtable when it grows past set-max-intset-entries 512 or set-max-listpack-entries 128. Sorted set: listpack to skiplist past 128 members or 64 bytes. List: listpack to quicklist past list-max-listpack-size -2, which is 8 KB. Every arrow points one way
Each type moves to its big encoding when a limit is crossed; for hashes, sets and sorted sets there is no way back while the server runs.
TypeSmallBigLimits (real default · on the page)
hashlistpackhashtablehash-max-listpack-entries 128 · 4, hash-max-listpack-value 64 · 8
setintset, then listpackhashtableset-max-intset-entries 512 · 8, set-max-listpack-entries 128 · 4, set-max-listpack-value 64 · 8 (listpack sets: Redis 7.2)
sorted setlistpackskiplist + dictzset-max-listpack-entries 128 · 4, zset-max-listpack-value 64 · 8
listlistpackquicklistlist-max-listpack-size -2 (8 KB per node) · 4 entries per node

The checks run on writes. HSET converts before the insert when a new field or value is too long, and after it when the count goes over the limit. ZADD converts before inserting the member that would be one too many (Demo: zset listpack → skiplist). A set holding integers turns into a listpack when a string member arrives, but only if it is still small enough; otherwise it goes straight to a hashtable.

The big encodings: a hashtable is a Redis dict where every field and value is its own SDS in a 24-byte dictEntry. A sorted set's skiplist encoding is a skiplist ordered by score plus a dict from member to score; see Redis sorted sets. The skiplist's header node alone has 32 levels.

Why the conversion is one-way

Delete fields from a hash that became a hashtable and it stays a hashtable, even with one field left (Demo: hash listpack → hashtable, the HDEL step). Redis does not check on deletes:

  • Converting back costs O(N) work, done on the main thread inside a command.
  • A key whose size moves up and down around the limit would be converted back and forth on every few writes.
  • A key that was big once is likely to be big again.

A key gets its compact form back only when it is rebuilt: a restart or DEBUG RELOAD loads every key from the RDB file and picks the smallest encoding that fits (the last step of that demo), and so does a replica's full sync. Lists are the exception: since Redis 7.2 a quicklist that shrinks to well under the limit turns back into a single listpack.

Lists: a quicklist of listpacks

A small list is a single listpack. A bigger one is a quicklist: a doubly linked list whose nodes each hold a listpack (Demo: list grows into quicklist nodes). list-max-listpack-size limits each node: a negative value is a size in bytes (-2 = 8 KB, the default), a positive value a number of entries (the page uses 4). Pushes and pops at either end touch only the end node, and a long list is never one huge block that has to be copied when it grows. list-compress-depth can also compress the nodes in the middle with LZF.

Memory per key, and the small-hashes trick

The ≈ bytes on the canvas are approximate: 64-bit pointers, the main structs, no allocator rounding (jemalloc rounds every allocation up to a size class, so real numbers are higher). Still, the order of magnitude is right:

ValueEncoding≈ bytes (with the key)
counter = 42int (shared)43
counter = 123456int59
counter = "hello"embstr68
user:1 with 3 short fieldslistpack95
the same 3 fields after one long value came and wenthashtable262

Each key has a fixed overhead (dictEntry, key name, robj). Many tiny keys pay it many times. The Redis memory guide suggests grouping them into small hashes: instead of user:1000 … user:1007, store hash users:0 with fields 1000 … 1003 and users:1 with the rest. Each hash stays a listpack as long as it has no more fields than hash-max-listpack-entries, so pick the bucket size below that limit. In Demo: small hashes vs separate keys 8 values take ≈ 543 bytes as string keys and ≈ 195 bytes as two hashes; one field too many turns a hash into a hashtable and most of the saving is gone. The trade-off: a hash field can't have its own TTL (before Redis 7.4's HEXPIRE), and very large listpacks get slow.

See also how Redis serves many connections with one thread (where these commands run), the Redis dict and incremental rehashing, Redis sorted sets and skiplists, expiry and eviction (the lru field), RDB and AOF persistence (what DEBUG RELOAD saves and loads), replication, streams (also built from listpacks), MULTI/EXEC, Redis Cluster, open hashing and Java's HashMap for chained hash tables, an LRU cache, and HyperLogLog, a Redis string with a sparse and a dense encoding of its own.

What the page leaves out

The real thresholds (128 entries, 64 bytes, 8 KB, 512 integers) and byte limits of a listpack (lpSafeToAdd, 1 GB); jemalloc size classes and the extra room sdsMakeRoomFor gets from them; the bigger SDS headers (sdshdr16, 32, 64); real hash functions (SipHash), incremental rehashing and dict shrinking; Redis 7.2's set dicts without values; skiplist levels drawn as towers and span counts; quicklist compression; the rule that shared integers are not used when maxmemory uses an LRU or LFU policy; the OBJ_ENCODING_STREAM encoding; Redis 8's new hash field expiry encodings (listpackex); and big-key costs like UNLINK freeing memory in a background thread.