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?
answer
- TYPE = logical, OBJECT ENCODING = layout
- listpack/intset = compact, O(n), tiny overhead
- hashtable/skiplist/quicklist = general, scales
- thresholds: entries and element size
- conversion is one-way, never back
basics
~20 sOBJECT 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 sEvery 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> 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
Name the compact versus general split, that OBJECT ENCODING reports it, and that thresholds drive the switch.
Add the specific encodings per type, the entries-and-value-size threshold pair, and the one-way conversion rule.
Connect it to real memory numbers and diagnosis — MEMORY USAGE, per-key overhead, latency from oversized listpacks.
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