jonnify·redis /redis-patterns/production-operations/twitter-internals/quicklist-and-memory

R8.04.2 · Twitter/X: internals & custom structures — dive 2

Quicklist & memory: pack the list, keep the nodes short.

A linked list spends 40+ bytes of overhead per entry; a ziplist 1 to 10 bytes but is costly to edit when long. Quicklist combines them — a doubly-linked list of ziplists (listpacks today) — and a measured figure makes the case: 200 lists of a million integers in 1.0 GB against 11.86 GB.

Grounding: the Twitter case study (Matt Stancliff, Redis Quicklist; redis/redis PR #2143) · the BCS echo — the wake list in echo_mq/lanes.ex. Valkey is the BCS engine; Twitter’s own Redis is the case study.

Three ways to store a Redis list. Select a representation to light it and read its per-entry overhead and edit cost — quicklist takes the best of both.
linked list one element per node · 40+ B overhead/entry ziplist | len | val | len | val | … | one packed block · 1–10 B/entry · costly to edit when long quicklist = linked list of ziplists ziplist 0 ziplist 1 … N packed nodes, kept short — low overhead + bounded edits
readout · Select a representation. A linked list is flexible but heavy per entry; a ziplist is compact but costly to edit when long; quicklist is a linked list of short ziplist nodes — compact and cheap to edit at once.

The quicklist shape (verbatim) — “a quicklist looks like: [ziplist 0] <-> [ziplist 1] <-> … <-> [ziplist N].”

§1

The memory problem

matt.sh · the two representations

A Redis list can be represented two ways, and each trades the other’s weakness.

A linked list holds one element per node, and each node carries pointers and allocator headers. Matt Stancliff puts the cost plainly: “Regular linked lists have overhead of 40+ bytes per entry.” For a list of small integers, the overhead dwarfs the data — forty-plus bytes of bookkeeping to hold an eight-byte value. The upside is cheap edits at either end: pushing or popping is a pointer update.

A ziplist packs many entries into one contiguous block of memory, with “overhead ranging from 1 byte to 10 bytes per entry.” That is an order of magnitude less than the linked list, because there are no per-element nodes — only lengths and the values, laid end to end. The downside is the cost of editing a long block: inserting into the middle, or growing past the allocation, can mean copying the whole block, so a single large ziplist becomes expensive as it grows.

So the two representations sit at opposite ends: the linked list is flexible but heavy per entry; the ziplist is compact but costly to edit when long. A list of a billion entries wants both — compact storage and cheap edits.

linked list
One element per node. 40+ B overhead/entry; cheap end edits.
ziplist
One packed block. 1–10 B overhead/entry; costly to edit when long.
the want
Compact storage and cheap edits — both at once.

The linked list and the ziplist each give up what the other keeps. Holding a billion list entries affordably needs the ziplist’s packing without the long block’s edit cost.

§2

Quicklist: a linked list of ziplists

matt.sh · the figure

Quicklist is the combination. It is “a quicklist looks like: [ziplist 0] <-> [ziplist 1] <-> … <-> [ziplist N]” — a doubly-linked list whose nodes are ziplists (listpacks in modern Valkey). Each node packs many entries compactly; the linked list of nodes keeps edits local to one node and keeps any single node short enough to edit cheaply.

# the quicklist shape (Matt Stancliff, verbatim)
[ziplist 0] <-> [ziplist 1] <-> … <-> [ziplist N]
#  each node packs many entries (1–10 B overhead/entry);
#  the linked list of nodes keeps edits local and nodes short.

The two costs are bounded at once: per-entry overhead stays near the ziplist’s — a few bytes per entry inside a node, not the linked list’s 40+, because entries are packed — and edit cost stays bounded, because a push or a middle insert touches one node, and a node is kept short, so no operation copies a billion-entry block.

The figure makes the saving concrete. Storing 200 lists of 1 million integers used 1.0 GB of allocated memory at the optimal ziplist length versus 11.86 GB for traditional linked lists — roughly twelve times less memory for the same data. At Twitter’s reported scale — “over 10,000 Redis servers per data center” holding “over 100 TB of live memory per datacenter” — a twelve-times reduction in the cost of a list is the difference between a fleet that fits and one that does not.

The figure: 200 lists of 1 million integers. Select an encoding to read its allocated memory — the same data, roughly twelve times apart.
quicklist 1.0 GB linked list 11.86 GB
readout · Select an encoding. The same 200 million integers cost 1.0 GB as a quicklist at the optimal ziplist length, against 11.86 GB as traditional linked lists — roughly twelve times less.

The figure is measured at the ziplist length that minimised allocated memory; a node’s length is the tuning dial — too short approaches a per-entry linked list, too long approaches an expensive-to-edit block.

Quicklist keeps the ziplist’s low per-entry overhead and bounds the edit cost by keeping each node short. The figure is the proof: 200 million integers in 1.0 GB, not 11.86 GB.

§3

The BCS echo — the wake list is one small node

echo_mq/lanes.ex

EchoMQ does not store million-entry lists, but it has one list whose whole purpose is to stay in a single node — the wake list. When a job is admitted, the enqueue script signals a parked consumer and trims the signal list to 64 elements:

echo/apps/echo_mq/lib/echo_mq/lanes.ex · the wake list, capped at 64frozen
-- signal a parked consumer, then hold the wake list to 64 elements
redis.call('LPUSH', KEYS[7], '1')
redis.call('LTRIM', KEYS[7], 0, 63)

A 64-element list of one-byte tokens lives as one small listpack node of a quicklist. That is the precise case quicklist and listpack were built for — a short list that should be one compact, cheap-to-edit block. The LTRIM … 0, 63 is what keeps it there: no matter how often work arrives, the wake list never grows past one node, so the signal stays cheap.

the pattern

Pack a list into ziplist/listpack nodes and keep each node short, so a list is both compact per entry and cheap to edit; a short list is a single node.

its EchoMQ application

The wake list is held to 64 elements with LTRIM … 0, 63, so it stays one small listpack node; the lane ring and the wake signal are native Redis lists, and the encoding that keeps them cheap is the one Twitter’s scale drove into core Redis.

The honest framing: EchoMQ does not configure quicklist — it cannot. Quicklist is the list encoding, and all Valkey lists are quicklists. The only thing the script controls is the cap, which keeps the wake list inside a single listpack node. The node-size thresholds that govern when any list splits across nodes are the subject of R7.02 (memory optimization) — the listpack and the limits that decide a node’s size.

the application · a list that stays one node

The 64-element wake list

The wake list is trimmed to 64 so it never grows past a single listpack node — the exact case the encoding optimises, verbatim from the lanes module:

echo/apps/echo_mq/lib/echo_mq/lanes.ex · one node by constructionfrozen
# the wake list: a 64-element Redis list of one-byte tokens
LPUSH wake '1'
LTRIM wake 0 63        # ≤ 64 elements → one listpack node

Notes on ValkeyIn modern Valkey a quicklist’s nodes are listpacks, which replaced ziplists in 7.0. The list-max-listpack-size setting bounds a node by entry count or by bytes; a list that stays under it is a single node. A 64-element wake list of one-byte tokens is far under any reasonable bound, so it is always one compact node — valkey.io/topics/data-types.

§4

Recap — compact entries, short nodes

next → R8.04.3

The two early list representations each gave up what the other kept: the linked list was flexible but spent 40+ bytes per entry; the ziplist was compact at 1–10 bytes per entry but costly to edit when long. Quicklist combined them — a linked list of ziplist (now listpack) nodes — and the figure proves it: 200 million integers in 1.0 GB against 11.86 GB. EchoMQ’s wake list is the small-scale echo: held to 64 elements with LTRIM … 0, 63, it is one compact node by design. The next dive reads the precise story of how that technique became core Redis — and who wrote it.

§5

References

sources · related

Sources

Related in this course