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.
Hover any cell for what it holds.
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).
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.
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).
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.
| encoding | first byte | test | holds | enc+data |
|---|---|---|---|---|
| 7-bit uint | 0xxxxxxx | b & 0x80 == 0x00 | 0 … 127 | 1 |
| 6-bit string | 10xxxxxx | b & 0xC0 == 0x80 | length 0 … 63 | 1 + len |
| 13-bit int | 110xxxxx | b & 0xE0 == 0xC0 | -4096 … 4095 | 2 |
| 12-bit string | 1110xxxx | b & 0xF0 == 0xE0 | length 64 … 4095 | 2 + len |
| 32-bit string | 0xF0 | b == 0xF0 | length 4096 … 2^32-1 | 5 + len |
| 16-bit int | 0xF1 | b == 0xF1 | -32768 … 32767 | 3 |
| 24-bit int | 0xF2 | b == 0xF2 | -8388608 … 8388607 | 4 |
| 32-bit int | 0xF3 | b == 0xF3 | -2147483648 … 2147483647 | 5 |
| 64-bit int | 0xF4 | b == 0xF4 | full signed 64-bit | 9 |
| EOF | 0xFF | b == 0xFF | terminator | 1 |
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.
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.
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.