R7.02 · Data Modeling & Memory — module hub
Memory optimization.
Reduce Redis memory consumption by leveraging compact encodings (listpack, intset), using Hashes for small object storage, and choosing memory-efficient data structures.
Redis holds everything in memory, so efficient usage is critical — understanding the
engine's internal encodings often saves 50–80% for small objects. The grounding is EchoMQ's memory
discipline (echo/apps/echo_mq): the 3-field job HASH stays a listpack, the
per-group wake list is LTRIM-capped at 64, the streams are XTRIM…MAXLEN ~-capped.
The structures are bounded by design, not tuned after the fact. The three dives take the encodings, the field
names, and the caps in turn.
listpack; a small all-integer Set is an intset; both convert to a
hashtable once they cross a threshold.Small object encoding
memory-optimization.md.txt · the compact layoutsRedis automatically uses compact encodings for small collections. A Hash with few fields is stored as a listpack — a single sequential byte array — rather than a hash table. The listpack eliminates the per-entry pointer and bucket overhead, which can consume up to 80% of memory for small objects. An all-integer Set under its threshold uses the even tighter intset — a sorted array of fixed-width integers, no hashing at all.
The engine switches from the compact encoding to the standard one when a structure crosses a configured threshold. The defaults are Valkey's, not EchoMQ's:
hash-max-listpack-entries 512 hash-max-listpack-value 64 list-max-listpack-size -2 zset-max-listpack-entries 128 zset-max-listpack-value 64 set-max-intset-entries 512
A Hash with fewer than 512 entries, where each value is under 64 bytes, uses the listpack encoding. EchoMQ's
job HASH carries exactly three fields — state, attempts, payload
(echo/apps/echo_mq/lib/echo_mq/jobs.ex) — far under 512, with short values, so Valkey stores
the row as a listpack. Not because EchoMQ tuned the thresholds (the committed
infra/valkey/conf/valkey.conf sets none), but because the row is deliberately minimal.
Notes on ValkeyA Hash is stored as a listpack until it
exceeds hash-max-listpack-entries (512) or any value exceeds hash-max-listpack-value
(64 bytes), when it converts to a hashtable; an all-integer Set is an intset until
set-max-intset-entries (512). The conversion is one-way —
valkey.io/topics/memory-optimization.
The cheapest memory win is to keep a structure small enough to stay in its compact encoding. The listpack-and-intset dive takes the layouts and the threshold flip apart.
The bucketing pattern
memory-optimization.md.txt · per-key overheadEach individual key has overhead — memory for the key name, the expiration metadata, and internal bookkeeping. For millions of small values, that per-key overhead dominates the values themselves. Instead of a String key per item, group items into Hash buckets:
# a String key per item — 1,000,000 keys of overhead SET user:1:name "John" SET user:2:name "Jane" # bucketed into Hashes — 1,000 keys, each a compact listpack HSET users:0 "1:name" "John" "2:name" "Jane" HSET users:1 "1001:name" "Alice"
With 1000 items per bucket you have 1000 Hash keys instead of 1,000,000 String keys, and each Hash stays small
enough for the compact encoding. For item ID 12345 with bucket size 1000:
bucket = 12345 / 1000 = 12, key = "users:12", field = "12345:name". The
per-key overhead is paid 1000 times, not a million.
Bucketing trades a million keys of overhead for a thousand compact Hashes — the largest gain in this chapter for datasets of many small items.
Key name length & integer encoding
memory-optimization.md.txt · names, numbers, TTLsEvery byte in a key name consumes memory, and the name is stored on every key. At scale, short keys matter:
user:profile:12345:display_name becomes u:12345:dn, or — better — a Hash
so the prefix is stored once rather than per field (HGET u:12345 dn). EchoMQ names its job-HASH
fields state/attempts/payload: short, but not abbreviated to
noise — small enough to keep the value under hash-max-listpack-value while staying
a word a reader recognizes.
Redis also uses an integer encoding for numeric strings, which share a pool of pre-allocated
small integers: SET counter "42" is efficient, SET counter "42 items" is not. When a
value can be a pure number, store it as one — EchoMQ's attempts holds the bare integer fence
advanced by HINCRBY, never "3 tries". And every key with a TTL costs extra memory for
the expiration; grouping keys that expire together under one Hash TTL avoids paying it per entry. EchoMQ's job
rows carry no TTL — a job lives until completed or reaped.
Names and numbers are paid per row: keep field names short and whole, store numbers as numbers, and avoid per-key TTLs where one shared TTL will do. The short-field-names dive carves this slice.
Measuring, eviction, and compression
memory-optimization.md.txt · measure · evict · compressMeasure before optimizing: INFO memory for the overall picture, MEMORY USAGE <key>
for one key, MEMORY STATS for samples, MEMORY DOCTOR for a report. Then change the
lowest-effort technique and measure again.
When memory is exhausted, the maxmemory-policy setting determines what happens. The cache policies
(allkeys-lru, allkeys-lfu, volatile-ttl, …) delete
keys to keep serving; noeviction returns errors on writes instead. For a
cache, LRU or LFU makes sense; for data that must not be lost, noeviction is the only safe posture
— which is exactly why EchoMQ runs it. That posture is taught in depth in R7.1 and is not re-taught here:
noeviction — refuse the write, do not drop the record
The eviction-policy menu and why a record of truth runs noeviction — memory pressure as a
loud error, not a vanished job — is the subject of the
noeviction dive of R7.1. This module
lists it as one memory technique and doors there for the depth.
Compression — CPU for memory
For large string values, compress in the application (gzip, lz4, snappy) before storing and decompress on read — effective above a few hundred bytes where the ratio is meaningful. EchoMQ does not compress its small job payload; the win is not there at three short fields, but a large opaque body is a candidate.
Memory work is a loop: measure, apply the lowest-effort technique, measure again. Eviction is a cache posture; a record of truth refuses the write instead, and large values are a compression candidate.
EchoMQ's memory discipline
jobs.ex · lanes.ex · stream.exEchoMQ applies three of these techniques by construction. Its job HASH stays a listpack because the row is three short fields. Its growing structures — the per-group wake list and the streams — are capped where they are written, so they cannot leak memory no matter the traffic. Select a structure to read how it is bounded:
LTRIM-capped at 64, and the streams
XTRIM…MAXLEN ~-capped — each bounded where it is written.Compact encodings and small objects save 50–80% of memory; a structure that is bounded cannot leak it.
The 3-field job HASH stays a listpack (far under the 512 / 64 thresholds); the per-group
wake list is LTRIM-capped at 64 (lanes.ex); the streams are
XTRIM…MAXLEN ~-capped (stream.ex). The ceiling is in the design, not a knob
turned later.
Memory optimization in Redis is mostly choosing a structure small enough to stay compact and bounding the ones that grow. EchoMQ does both: a deliberately minimal row, and lists and streams capped where they are written.
EchoStore — the compact near-cache
A near-cache leans on the same compact encodings to keep its hot copies small. EchoStore — L1 ETS over L2 Valkey, coherence by mint time — is the Branded Component System's account of the tier that holds copies, where the memory budget is the live working set.
The maxmemory 512mb guardrail
The committed valkey.conf sets maxmemory 512mb as a loud guardrail far above the
working set, not a working ceiling. The production posture of that config — the release image, Valkey on
a Fly machine — is the Branded Component System production chapter.
The three dives
listpack-and-intset → short-field-names → capped-structuresEach dive takes one slice of the memory discipline, in order: the compact encodings and the threshold flip, the field names that keep the row in them, and the caps that bound the structures that grow. Read them in order — the encoding first, the names second, the caps third.
listpack and intset
A small Hash becomes a listpack (a sequential byte array, no bucket overhead); a small all-integer Set becomes an intset; the thresholds; the 3-field job HASH as the worked example.
short field names
Few, short fields keep the value under hash-max-listpack-value and the row compact;
EchoMQ's state/attempts/payload (short, not abbreviated); the trade,
multiplied by N rows.
capped structures
Bound a structure so it cannot leak: LTRIM list 0 N (the wake list at 64) and
XTRIM…MAXLEN ~ (the stream); the ~ accuracy-for-speed trade.
The three dives ground in the same real structures: the listpack row, the short fields, and the caps. Begin with listpack and intset. Return to the Data Modeling & Memory chapter to continue.
References
sources · relatedSources
- Valkey — Memory optimization — listpack and intset encodings, the configuration thresholds, and how small collections are stored compactly.
- Valkey — OBJECT ENCODING — read the internal encoding of a key (
listpack,intset,hashtable). - Valkey — LTRIM — trim a List to a range so it becomes a bounded ring; the wake-list cap.
- Valkey — XADD — append to a stream with
MAXLEN ~, the approximate cap that trims in whole macro-nodes.
Related in this course
- R7.02.1 · listpack and intsetthe compact encodings and the threshold flip
- R7.02.2 · short field namesfew short fields keep the row compact
- R7.02.3 · capped structures
LTRIMandMAXLEN ~bound memory - R7.01.2 · noevictionthe eviction posture for a record of truth
- /bcs · The storeEchoStore — the compact near-cache tier
- /bcs · Production on Flythe
maxmemory 512mbguardrail