← index

listpack

src/listpack.c · Redis 7.2.14

A listpack is one contiguous allocation: a six-byte header, a run of entries, and a single terminator byte. Every entry carries its own length at both ends, which is what lets a reader walk it backwards. Type values below and watch the bytes; the page then validates its own output with the same arithmetic Redis uses.

input

comma separated, trimmed

byte layout

—
header 4B total-bytes + 2B num-elements, little-endian encoding type byte, plus inline length or integer data element bytes, stored verbatim backlen this entry's encoding+data length EOF 0xFF

Hover any cell for what it holds.

self-check

—

Walks the bytes above exactly as lpValidateNext does (listpack.c:1292–1338): read the encoding, derive the entry length, step over it, then decode the backlen that sits immediately to the left of the next entry and require prevlen + encodedBacklen == entrylen (listpack.c:1331–1332).

traversal

—

lpPrev (listpack.c:485–496) steps one byte left of the current entry, lands on the last byte of the previous entry's backlen, decodes it right to left, then subtracts that length plus the backlen's own width.

size

header—
encoding bytes—
element data—
backlen bytes—
terminator—
total—

hex dump

offset, bytes, printable ASCII

per element

how the format works

header

LP_HDR_SIZE is 6 (listpack.c:48): four bytes of total size then two bytes of element count, both little-endian (lpGetTotalBytes, listpack.c:106–109; lpGetNumElements, listpack.c:111–112). Total size counts the header and the terminator, which is why an empty listpack is 7 bytes (lpNew, listpack.c:245).

The count field is a cache, not a guarantee. LP_HDR_NUMELE_UNKNOWN is 65535 (listpack.c:49); once the count reaches it, lpInsert stops incrementing (listpack.c:898) and lpLength answers by scanning the whole listpack instead (listpack.c:519), re-caching only if the count later drops back below the sentinel (listpack.c:532).

entry

An entry is an encoding byte, optional inline length bytes, the element data, and a backlen. Integer elements have no separate data: the value lives in the encoding bytes.

encodingfirst bytetestholdsenc+data
7-bit uint0xxxxxxxb & 0x80 == 0x000 … 1271
6-bit string10xxxxxxb & 0xC0 == 0x80length 0 … 631 + len
13-bit int110xxxxxb & 0xE0 == 0xC0-4096 … 40952
12-bit string1110xxxxb & 0xF0 == 0xE0length 64 … 40952 + len
32-bit string0xF0b == 0xF0length 4096 … 2^32-15 + len
16-bit int0xF1b == 0xF1-32768 … 327673
24-bit int0xF2b == 0xF2-8388608 … 83886074
32-bit int0xF3b == 0xF3-2147483648 … 21474836475
64-bit int0xF4b == 0xF4full signed 64-bit9
EOF0xFFb == 0xFFterminator1

Macros at listpack.c:55–97. Integer payloads and the 12- and 32-bit string lengths are little-endian (lpEncodeIntegerGetType, listpack.c:267–321; lpEncodeString, listpack.c:403–419). Those widths exclude the backlen, so an entry costs one byte more than the table's last column for anything under 128 bytes; Redis's own LP_ENCODING_*_ENTRY_SIZE constants (listpack.c:58, 67, 76, 81, 86, 91) include it.

backlen

The trailing field of an entry encodes that entry's own encoding+data length, not the previous entry's, and not including the backlen bytes themselves. lpInsert calls lpEncodeBacklen(backlen, enclen) with the length of the element it just encoded (listpack.c:831) and writes it directly after the data (listpack.c:891). The validator's arithmetic is the proof: it requires prevlen + encodedBacklen == entrylen (listpack.c:1331–1332).

The layout is built to be read backwards:

That is what makes lpPrev possible at all: standing at the start of an entry, the bytes immediately to the left are the tail of the previous entry's backlen, and decoding them right to left yields how far back to jump.

integer or string

An element is stored as an integer only when lpStringToInt64 accepts it (listpack.c:331), and that function is deliberately strict (listpack.c:176–238): it rejects anything 21 bytes or longer (line 183), a lone minus sign (line 195), any first digit that is not 1–9 unless the whole string is exactly 0 (lines 200–206), trailing non-digits (line 221), and any value outside signed 64-bit range (lines 210, 214, 226, 230).

So 007, -0 and 99999999999999999999 are all stored as strings. The rule exists so that decoding an integer entry reproduces the original string byte for byte.