skip to content

Core Data Structures

You will learn every native Redis type — strings, hashes, lists, sets, sorted sets, bitmaps, HyperLogLog, geospatial — plus the internal encodings that make them cheap. Interviewers probe this to see whether you pick the right structure and can predict its time and memory cost instead of treating Redis as a plain key-value store.

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

explore

questions

page 1 of 2

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

How would you store a user profile in a Redis hash, and which commands read a single attribute, several attributes, and the whole object? What happens to the key when you delete its last field?

level: juniorimportance: must knowfreq 68%

basics

~20 s

HSET user:42 name Ada email [email protected] writes fields; HGET reads one, HMGET reads several in one call, HGETALL returns every field and value. HDEL removes fields, and when the last field goes the key itself is deleted — Redis has no empty hashes.

open as a page

Describe the Redis List type: which commands you use to add and remove elements, how you read a range out of it, and how the cost of those operations differs between the ends of the list and the middle.

level: juniorimportance: must knowfreq 70%

basics

~20 s

An ordered sequence of strings with fast ends. LPUSH/RPUSH add at head/tail, LPOP/RPOP remove there, all O(1). LRANGE reads a slice; LINDEX/LSET/LINSERT/LREM touch the middle and are O(N) because Redis walks the list. Duplicates are allowed, order is insertion order, and an emptied list key is deleted.

open as a page

What is a Redis Set, and which commands do you use to add a member, remove one, test membership and get the element count?

level: juniorimportance: must knowfreq 72%

basics

~20 s

A Set is an unordered collection of unique strings. SADD adds (duplicates are silently ignored), SREM removes, SISMEMBER tests membership, SCARD returns the size, SMEMBERS returns everything. Add, remove, membership test and count are all O(1).

open as a page

What is a Redis Sorted Set, how is the ordering of its elements defined, and which commands do you use to add an element, read its score, read its rank and read a range?

level: juniorimportance: must knowfreq 74%

basics

~20 s

A Sorted Set holds unique string members, each with a floating-point score, kept ordered by score ascending and by member bytes when scores tie. ZADD adds or updates, ZSCORE reads a score, ZRANK gives the 0-based position, ZRANGE reads a slice, ZCARD counts.

open as a page

The Redis SET command accepts NX, XX, EX/PX, KEEPTTL and GET options. What does each of them do, and why is `SET key value NX EX 30` preferred over calling SETNX and then EXPIRE?

level: juniorimportance: must knowfreq 72%

basics

~20 s

NX writes only if the key is absent, XX only if it already exists, EX/PX attach a TTL in seconds/milliseconds, KEEPTTL keeps the current TTL, GET returns the previous value. A single SET is one atomic command; SETNX then EXPIRE can leave a key with no TTL if the client dies between them.

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 storing an object in Redis, what do you gain and lose by modelling it as a hash with one field per attribute versus serialising the whole object to JSON and storing it in a single string key?

level: middleimportance: must knowfreq 58%

basics

~20 s

A hash gives partial reads and writes (HGET/HSET one field), atomic per-field updates like HINCRBY, and compact memory for small objects. A JSON string gives one round trip, nesting and arrays, and atomic whole-object replacement — but every update is a read-modify-write that can lose concurrent changes.

open as a page

A Redis Set holds several million members. What does SMEMBERS cost the server and the client, and how would you satisfy the common requests against that set — its size, whether specific values are in it, a random sample, the size of its overlap with another set — without ever materialising it?

level: middleimportance: must knowfreq 58%

basics

~20 s

SMEMBERS is O(N): one giant reply that occupies the single command thread and can blow the client output buffer, killing the connection. Ask set-native questions instead — SCARD for size, SISMEMBER/SMISMEMBER for membership, SRANDMEMBER for a sample, SINTERCARD with LIMIT for overlap size.

open as a page

Design a game leaderboard on Redis Sorted Sets: how do you record a score change, fetch the top ten, and show one player their own position and the players around them?

level: middleimportance: must knowfreq 66%

basics

~20 s

Store one Sorted Set of player→score. Record with ZINCRBY for deltas or ZADD GT for high-water marks. Top ten is ZRANGE key 0 9 REV WITHSCORES. A player's position is ZREVRANK (0-based, add 1 for display), and neighbours are a small ZRANGE REV around that rank.

open as a page

Why is Redis's INCR the correct way to implement a counter shared by many clients, and what happens if the key is missing, holds non-numeric text, or is incremented past the 64-bit range?

level: middleimportance: must knowfreq 68%

basics

~20 s

INCR performs the read-modify-write inside Redis as one command, so concurrent clients cannot lose updates. A missing key is treated as 0 and created. A non-numeric or out-of-range value returns an error rather than resetting. Values are signed 64-bit; overflow errors instead of wrapping. INCR keeps any existing TTL.

open as a page

Implement a sliding-window rate limiter using a Redis Sorted Set — which commands run, in what order, and what are the failure modes you have to design around?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Per subject, keep a Sorted Set whose scores are request timestamps. On each request: ZREMRANGEBYSCORE to drop entries older than the window, ZCARD to count, ZADD a uniquely-named entry if under the limit, and PEXPIRE the key. Run all four atomically in one Lua script.

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

Redis geospatial commands are often described as a thin layer over another core Redis type. Which type actually backs GEOADD, how is a longitude/latitude pair encoded into it, and what practical consequences does that have for how you operate the key?

level: middleimportance: should knowfreq 30%

basics

~20 s

A sorted set. GEOADD interleaves 26 bits of longitude and 26 bits of latitude into one 52-bit geohash integer and stores it as the member's score. So ZREM, ZSCORE, ZCARD, ZRANGE, DUMP and EXPIRE all work on a geo key, and nearby-search is a set of score-range scans.

open as a page

You are keeping per-endpoint request counters grouped under one Redis key. How does HINCRBY behave when the key or the field does not exist yet, when the stored value is not a number, and how does it compare with reading the field and writing it back from the application?

level: middleimportance: should knowfreq 42%

basics

~20 s

HINCRBY treats a missing key or field as 0, creates it, and returns the new value. It errors if the value is not a 64-bit integer or if the result would overflow. It is one atomic command, so unlike HGET-then-HSET it cannot lose concurrent increments.

open as a page

You want to show a random sample of fields from a large Redis hash without pulling the whole thing. How does the Redis command HRANDFIELD serve that, how does its reply shape change with the WITHVALUES option under RESP2 versus RESP3, and how does it compare with HSCAN for the same job?

level: middleimportance: should knowfreq 22%

basics

~20 s

HRANDFIELD key count returns random field names at O(count) cost, independent of how big the hash is. WITHVALUES adds each value: a flat field,value,field,value array under RESP2, a map under RESP3. It samples; HSCAN enumerates the whole hash. Its order is not pagination.

open as a page

You keep one Redis HyperLogLog per day of unique visitors. How do you produce a weekly and a monthly unique-visitor number, and what does the PFMERGE command guarantee about the accuracy of the result?

level: middleimportance: should knowfreq 40%

basics

~20 s

Union the daily keys: PFCOUNT day1 day2 ... estimates the union directly, or PFMERGE week day1 day2 ... stores a reusable weekly key. Merging takes the max of each register, so the merged key equals one built from all raw elements; error stays ~0.81% and does not accumulate across merges.

open as a page

Redis exposes a HyperLogLog type through the PFADD and PFCOUNT commands. What problem does it solve, and what do you give up compared with putting the same items into a Redis Set and calling SCARD?

level: middleimportance: should knowfreq 45%

basics

~20 s

It counts unique items approximately in fixed memory. PFADD adds an element; PFCOUNT returns an estimated distinct count with about 0.81% standard error, using at most ~12 KB per key however many items you add. Unlike a Set it stores no members, so you cannot list them, test membership, or remove one.

open as a page

Redis offers blocking list commands such as BLPOP and BLMOVE. Explain what blocking means here, what happens to the server and to other clients while one client is blocked, and how these commands behave inside a MULTI/EXEC transaction.

level: middleimportance: should knowfreq 52%

basics

~20 s

BLPOP parks the calling client until an element arrives on one of the given keys or the timeout expires, returning key and value, or nil on timeout. Only that client waits; the server keeps serving everyone else. Inside MULTI/EXEC and Lua they never block — they behave like the non-blocking version and return nil immediately.

open as a page

You want a Redis key holding only the 100 most recent events for each user, without it growing forever. Show how LPUSH together with LTRIM implements that, and explain what the trim actually costs and why the two commands should be issued together.

level: middleimportance: should knowfreq 42%

basics

~20 s

Push then trim: LPUSH feed:42 event, then LTRIM feed:42 0 99, which keeps only that index range and discards the rest. Send both in one MULTI/EXEC or Lua script so the list is never read oversized and the trim cannot be skipped. LTRIM is O(N) in elements removed, so trimming on every push removes about one element.

open as a page

Redis offers SINTER, SUNION and SDIFF plus SINTERSTORE, SUNIONSTORE and SDIFFSTORE. How do these behave, what do they cost, and when would you reach for the STORE variants?

level: middleimportance: should knowfreq 52%

basics

~20 s

They compute intersection, union and difference of Sets on the server. SINTER is roughly O(smallest set × number of sets), SUNION/SDIFF O(total members). STORE variants write the result into a destination key and return its size instead of shipping members — the destination is overwritten and gets no TTL.

open as a page

Redis has both SPOP and SRANDMEMBER for getting random elements out of a Set. How do they differ, and how does supplying a count — including a negative one — change what you get back?

level: middleimportance: should knowfreq 42%

basics

~20 s

SPOP removes and returns random members; SRANDMEMBER returns them without removing. With a positive count both return distinct members, capped at the set size. Only SRANDMEMBER accepts a negative count, which allows repeats and returns exactly that many elements.

open as a page

Explain the ZADD flags NX, XX, GT, LT, CH and INCR in Redis, and give a situation where each one changes the outcome.

level: middleimportance: should knowfreq 40%

basics

~20 s

NX only inserts new members, XX only updates existing ones, GT and LT update only if the new score is greater or lesser. CH changes the reply to count added-or-changed members. INCR makes ZADD behave like ZINCRBY and return the new score, or nil if a condition blocked it.

open as a page

Redis has no dedicated bitmap type — SETBIT, GETBIT and BITCOUNT operate on ordinary strings. How does that work, how much memory does a daily-active-user flag for 50 million user ids cost, and what is the trap when the bit offsets are sparse?

level: middleimportance: should knowfreq 42%

basics

~20 s

A bitmap is just a string addressed bit by bit. SETBIT key offset 0|1 sets a bit and returns the old one; BITCOUNT counts set bits. 50 million bits is about 6 MB. The trap: setting a high offset allocates every byte below it, so a single sparse id can allocate hundreds of MB.

open as a page

Redis strings can be modified in place with APPEND, SETRANGE and read partially with GETRANGE. How do these commands behave — including what SETRANGE does when you write past the end of the value — and what are the limits of treating a Redis string as a growable buffer?

level: middleimportance: should knowfreq 36%

basics

~20 s

A Redis string is a mutable byte array. APPEND adds bytes to the end and returns the new length. SETRANGE overwrites bytes at an offset, zero-filling any gap. GETRANGE returns a byte slice and accepts negative indexes. Max length is 512 MB, and rewriting huge values gets expensive.

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

Redis 7.4 added the HEXPIRE command family. What problem does expiring individual hash fields solve, how do you use those commands, and how did teams handle the same requirement before they existed?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Before 7.4, TTL applied only to a whole key, so one stale attribute forced you to expire the entire object. HEXPIRE key ttl FIELDS numfields f1 f2 sets a TTL per field; HTTL reads it, HPERSIST clears it, and the key is deleted when its last field expires. Older workarounds: one key per field, or a timestamp index swept by a job.

open as a page

A single Redis hash has grown to several million fields and application code calls HGETALL on it on every request. What goes wrong, and how would you read, shrink and eventually restructure that key?

level: seniorimportance: should knowfreq 34%

basics

~20 s

HGETALL is O(fields) and runs as one command, so it stalls the server for everyone and builds a huge reply in the client output buffer. Read with HMGET for known fields or HSCAN in chunks, delete with UNLINK or batched HDEL, and split the object across several keys so no single key is that large.

open as a page

showing 1–30 of 42