skip to content

Internal Encodings

You will learn that each Redis type silently switches between compact and full encodings — listpack to skiplist, intset to hashtable — as it grows. Interviewers ask because knowing the conversion thresholds is how you explain why 'the same data' can cost five times the memory.

part ofRedisoverview, primer and where to startread it →
on this pageshow

questions

6

Running OBJECT ENCODING on a small Redis hash returns "listpack", and on a large one it returns "hashtable". What is that command reporting, and why does Redis keep more than one internal representation for the same data type?

level: juniorimportance: must knowfreq 50%

answer

  1. TYPE = logical, OBJECT ENCODING = layout
  2. listpack/intset = compact, O(n), tiny overhead
  3. hashtable/skiplist/quicklist = general, scales
  4. thresholds: entries and element size
  5. conversion is one-way, never back

basics

~20 s

OBJECT ENCODING reports the internal memory layout Redis chose for that value. Small collections use a compact, cache-friendly flat array (listpack/intset) that saves a lot of memory; once they grow past configured thresholds Redis switches to a real hash table or skiplist for O(1)/O(log n) access.

solid answer

~50 s

Every Redis value has a **logical type** (string, list, hash, set, sorted set) and, underneath it, one of several **encodings** — the actual in-memory layout. `OBJECT ENCODING <key>` tells you which one is in use. The reason for more than one is a memory-versus-CPU trade. A real hash table costs pointers, bucket arrays and per-entry allocation headers — easily 50–100 bytes of overhead per entry. For a hash with five small fields, that overhead dwarfs the data. So Redis stores small collections as a **listpack**: a single contiguous blob holding the entries back to back, with almost no per-entry overhead and excellent CPU-cache behaviour. Lookup is a linear scan, but scanning ten entries in one cache line beats a pointer chase. When the collection exceeds configured thresholds — entry count or element size — Redis converts to the general structure (`hashtable`, `skiplist`, `quicklist`) whose asymptotics hold at scale. Typical encodings: strings `int`/`embstr`/`raw`; hashes and sorted sets `listpack`/`hashtable`/`skiplist`; sets `intset`/`listpack`/`hashtable`; lists `listpack`/`quicklist`.

code

text · 17 lines
text
> CONFIG GET hash-max-listpack-entries
1) "hash-max-listpack-entries"
2) "128"

> HSET user:1 name alice age 30
> OBJECT ENCODING user:1
"listpack"

# push it past 128 fields
> for i in $(seq 1 200); do redis-cli HSET user:1 f$i v$i; done
> OBJECT ENCODING user:1
"hashtable"

# delete back down -- encoding does NOT revert
> HDEL user:1 f1 f2 ... f200
> OBJECT ENCODING user:1
"hashtable"

go deeper

for a junior

Name the compact versus general split, that OBJECT ENCODING reports it, and that thresholds drive the switch.

for a middle

Add the specific encodings per type, the entries-and-value-size threshold pair, and the one-way conversion rule.

for a senior

Connect it to real memory numbers and diagnosis — MEMORY USAGE, per-key overhead, latency from oversized listpacks.

for a principal

Frame encoding as the memory cost model of an in-memory store, and drive key-design standards (grouping small keys into hashes) from it.

## Type versus encoding Redis exposes five core data types plus a few specialised ones. `TYPE mykey` returns the logical type — what commands you may run. `OBJECT ENCODING mykey` returns something different: the concrete memory layout the server picked for that particular value right now. The same `HSET`/`HGET` API works identically whichever encoding is underneath; the encoding is an invisible optimisation. ``` > RPUSH mylist a b c > OBJECT ENCODING mylist "listpack" > TYPE mylist list ``` ## Why a general structure is wasteful for small values A hash table in C is an array of buckets holding pointers to entries; each entry holds a key pointer, a value pointer and a next pointer. Add the allocator's per-allocation header and the Redis string (SDS) header on both key and value, and a single field/value pair of a few bytes can cost on the order of 50–100 bytes of *overhead*. For a hash representing a user session with six short fields, that is an order of magnitude more memory than the payload. This matters more in Redis than in most systems because Redis is an in-memory store: RAM *is* the cost model, and real deployments hold tens or hundreds of millions of small objects. A representation that halves per-object overhead halves the bill. ## The compact encodings - **listpack** — a single contiguous byte blob containing entries one after another, each prefixed with its own length metadata so the blob can be traversed in either direction. There are no pointers and one allocation for the whole collection. Used for small hashes, small sorted sets, small sets, and small lists. It replaced the older **ziplist**, which had the same idea but a design that made some updates prone to cascading size recalculation. - **intset** — for sets whose members are *all* integers: a sorted array of fixed-width integers (16, 32 or 64 bit, upgraded as needed). Membership is a binary search; memory is essentially just the numbers. - **embstr** — for short strings (44 bytes or fewer): the object header and the string data are allocated together in one block, so creating and freeing it is one allocation instead of two. Longer strings use `raw` (two allocations); strings that are valid integers are stored as `int`, a plain long inside the object. The cost of a compact encoding is that operations are **O(n) in the number of entries**, since you scan the blob. That is deliberately fine because n is capped by configuration — and for small n, a linear walk over contiguous memory is faster in wall-clock terms than hashing plus a pointer dereference, because it touches one or two cache lines instead of jumping around the heap. ## The general encodings - **hashtable** — the real dict, used for large hashes and large sets; O(1) average lookup, incremental rehashing when it grows. - **skiplist** — used for large sorted sets, giving O(log n) range and rank operations. A sorted set in `skiplist` encoding actually keeps *two* structures: the skiplist for ordering and a dict for O(1) member → score lookup. - **quicklist** — used for large lists: a doubly linked list whose nodes are themselves listpacks, so you get cheap push/pop at both ends without one allocation per element. ## How and when Redis switches Conversion is driven by per-type configuration thresholds — element count and the size of any individual element (for example `hash-max-listpack-entries` and `hash-max-listpack-value`, defaulting to 128 and 64). When a write pushes a collection past either limit, Redis converts the value in place to the general encoding. Two properties matter in practice. First, the check is on *any* element: one 200-byte field converts an otherwise tiny hash. Second, **conversion is one-way**: delete elements back down below the threshold and the encoding stays `hashtable`. Redis does not downgrade, because oscillating around the boundary would cost more than it saves. To get the compact encoding back you must recreate the key. ## Why you should care As a developer you rarely choose an encoding directly, but encoding explains two very common observations: why a million tiny keys use far more memory than the sum of their values (top-level keys always carry full object overhead, so grouping them into hashes that stay listpack-encoded is a large win), and why one key that looks small behaves slowly (a listpack that was allowed to grow, or a value stuck in a general encoding). `OBJECT ENCODING` plus `MEMORY USAGE <key>` is the first pair of commands to reach for when memory or latency surprises you.

  • If a listpack lookup is O(n), why is it not slower than a hash table for small collections?
    Because n is bounded by configuration, typically 128 entries, and the listpack is one contiguous allocation. Scanning it touches a couple of cache lines sequentially, which modern CPUs prefetch well, whereas a hash table costs a hash computation plus a pointer dereference into a randomly located entry — likely a cache miss. Asymptotics only win once n is large enough for the constant factors to stop dominating.
  • A hash was converted to hashtable and then shrunk back to three fields. What encoding does it have, and how would you reclaim the memory?
    It stays hashtable — Redis never converts back, because a value oscillating across the threshold would pay repeated conversion cost. The only way to regain the compact encoding is to recreate the key: read the remaining fields and write them to a fresh key, for example with a rename after rebuilding, or simply delete and re-populate from the source of truth.

A shopping list of five items fits on one sticky note you read top to bottom; a warehouse inventory needs an indexed catalogue. Redis keeps the sticky note until the list outgrows it, then builds the catalogue — and never goes back to the sticky note.

saying these in an interview costs you the question

  • Confusing TYPE with OBJECT ENCODING, or thinking the encoding changes the available commands
  • Believing Redis converts back to the compact encoding when a collection shrinks
  • Assuming O(n) listpack access is always slower than a hash table
  • Thinking encodings are a legacy detail with no memory impact
  • Saying the encoding is chosen once at key creation and never changes

context

open as a page

Which Redis configuration settings decide whether a hash or sorted set keeps its compact internal layout, and what exactly happens at the boundary?

level: middleimportance: must knowfreq 52%

basics

~20 s

hash-max-listpack-entries / hash-max-listpack-value and zset-max-listpack-entries / zset-max-listpack-value, defaulting to 128 entries and 64 bytes. Exceeding either — too many entries, or one element longer than the value limit — converts the value to hashtable or skiplist, permanently.

open as a page

When a Redis sorted set grows past its compact threshold, OBJECT ENCODING reports "skiplist". What two structures does that encoding actually maintain, and why does it need both?

level: middleimportance: should knowfreq 38%

basics

~20 s

It keeps a skiplist ordering members by score for O(log n) rank and range queries, plus a hash table mapping member to score for O(1) lookups and updates. Neither structure alone gives both ordered access and constant-time member lookup.

open as a page

A Redis instance holds 50 million tiny key-value pairs and uses far more memory than the sum of the values suggests. How would you investigate, and what restructuring would let the encoding layer cut that footprint?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Each top-level key carries its own object, SDS and dict-entry overhead — often 50-100 bytes regardless of value size. Group the records into hashes bucketed so each stays under hash-max-listpack-entries/value, so many records share one compact listpack. Measure with MEMORY USAGE, OBJECT ENCODING and MEMORY DOCTOR.

open as a page

A Redis list of a million elements reports the "quicklist" encoding while a three-element list reports "listpack". Explain how a quicklist is built and what list-max-listpack-size and list-compress-depth control.

level: seniorimportance: should knowfreq 30%

basics

~20 s

A quicklist is a doubly linked list whose nodes are each a listpack holding many elements. list-max-listpack-size caps a node by element count (positive) or by bytes (negative, e.g. -2 = 8 KB). list-compress-depth LZF-compresses interior nodes, leaving that many nodes uncompressed at each end.

open as a page

Your team proposes raising hash-max-listpack-entries and zset-max-listpack-entries from the default 128 to 4096 across the whole Redis fleet to cut memory cost. How would you evaluate that proposal, and what would you set instead?

level: principalimportance: nice to knowfreq 24%

basics

~20 s

It trades RAM for CPU on the single command thread: listpack access is a linear scan and writes may rewrite the whole blob, so 4096-entry values raise tail latency for every access. Evaluate per workload with measured memory and p99, set per-instance rather than fleet-wide, and prefer a moderate bump plus a hard cap on element size.

open as a page