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.
The quicklist shape (verbatim) — “a quicklist looks like: [ziplist 0] <-> [ziplist 1] <-> … <-> [ziplist N].”
The memory problem
matt.sh · the two representationsA 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.
Quicklist: a linked list of ziplists
matt.sh · the figureQuicklist 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 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.
The BCS echo — the wake list is one small node
echo_mq/lanes.exEchoMQ 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:
-- 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.
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.
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 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:
# 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.
Recap — compact entries, short nodes
next → R8.04.3The 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.
References
sources · relatedSources
- Matt Stancliff — Redis Quicklist — From a More Civilized Age — the encoding’s author on the memory problem (linked-list 40+ B/entry vs ziplist 1–10 B/entry), the
[ziplist 0] <-> … <-> [ziplist N]shape, and the 1.0 GB vs 11.86 GB figure for 200 lists of 1 million integers. - redis/redis — Quicklist (linked list + ziplist), PR #2143 — the pull request that brought quicklist into core Redis, shipped in 3.2.
- Valkey — Data types — the list as a quicklist of listpack nodes, and the
list-max-listpack-sizethreshold that keeps a short list in one node.
Related in this course
- R8.04 · Twitter/X: internals & custom structuresthe module hub
- R8.04.1 · Timeline fan-outthe previous dive — the per-follower Redis lists
- R8.04.3 · What became corethe next dive — the technique that became quicklist
- R7.02 · Memory optimizationthe listpack node-size thresholds behind the wake list
- /echomq/queuethe Queue pillar — jobs, lanes, the lists behind the bus