jonnify·redis /redis-patterns/data-modeling/memory-optimization/capped-structures

R7.02.3 · dive 3

A bounded structure cannot leak.

A compact encoding saves bytes on a structure; a cap bounds how many bytes it can ever take. A List or Stream that grows without limit is a memory leak waiting for traffic.

The fix is to trim it where it is written: LTRIM list 0 N keeps a List to its newest N; XTRIM…MAXLEN ~ keeps a Stream to roughly its newest N. EchoMQ does both — the per-group wake list is capped at 64, the streams are MAXLEN ~-capped — so the ceiling is a property of the code, not a hope about volume. Step the list past the cap; watch LTRIM clamp it.

Grow, then cap. Push entries onto the list; LTRIM list 0 63 keeps at most 64 and discards the rest, so the size is fixed no matter the traffic.
— cap: LTRIM 0 63 (at most 64)
pushed onto the list (grows with traffic) — kept after LTRIM 0 63 (capped at 64) —
§1

The unbounded structure is the hazard

memory-optimization.md.txt · the leak

Lists and Streams are append-friendly: LPUSH/RPUSH and XADD are O(1), so it is easy to write to them on a hot path and never remove. Left alone, they grow with traffic forever, and a structure that only grows will eventually exhaust memory — at which point, under noeviction, writes start failing. A cap removes the hazard at the source: bound the structure when you write it, and its size is fixed no matter how much traffic arrives.

A cap is also when a List or Stream leaves its compact encoding behind safely. The point of a cap is not the encoding, though — it is the absolute ceiling: a capped structure cannot leak, by construction.

An append-only structure with no cap is a memory leak waiting for traffic. Bound it where it is written and its size is fixed.

§2

LTRIM — the wake list at 64

echo_mq/lanes.ex · the bounded ring

EchoMQ's fair-lanes layer keeps a per-group wake list: a short signal list a parked consumer watches, written on the hot admission path. If it were unbounded it would grow with every admitted job. It is not — it is capped the moment it is pushed:

echo/apps/echo_mq/lib/echo_mq/lanes.ex · the wakefrozen
-- the grouped-admission script, the wake bookkeeping
redis.call('LPUSH', KEYS[7], '1')
redis.call('LTRIM', KEYS[7], 0, 63)

LPUSH adds a marker; LTRIM KEYS[7], 0, 63 immediately keeps only indices 0 through 63 — at most 64 entries — and discards the rest. The list is a bounded ring, never an unbounded log: one wake is enough to rotate a parked consumer back to service, so 64 markers is generous headroom and the memory is fixed. The moduledoc frames the layer: “the ring is the rota — a list holding exactly the lanes that can be served right now.”

Notes on ValkeyLTRIM key start stop keeps only the elements in the inclusive range and removes the rest, so LPUSH then LTRIM 0 63 is the idiom for a bounded ring of at most 64 entries — valkey.io/commands/ltrim.

The cheapest cap is a fixed-length ring: LPUSH the new entry, LTRIM 0 N to keep the newest N. EchoMQ's wake list is one such ring, bounded at 64 where it is written.

§3

XTRIM…MAXLEN ~ — the stream cap

echo_mq/stream.ex · trim/4 · the approx trade

A Stream is the retained, replayable log. EchoMQ trims it to a declared retention window over XTRIM, issued directly:

echo/apps/echo_mq/lib/echo_mq/stream.ex · trim/4frozen
# the maxlen window — XTRIM issued direct, no new Lua
def trim(conn, queue, name, {:maxlen, count, approx?})
    when is_binary(queue) and is_binary(name) and is_integer(count) and count >= 0 and
           is_boolean(approx?) do
  key = stream_key(queue, name)
  xtrim(conn, ["XTRIM", key, "MAXLEN", approx_flag(approx?), Integer.to_string(count)])
end

defp approx_flag(true), do: "~"
defp approx_flag(false), do: "="

The third argument, approx?, selects the trim mode — and it is the memory-vs-exactness trade. Select a mode to see where each trims:

MAXLEN ~ vs MAXLEN =. Select a mode; the readout reports where the trim boundary falls and what the trade buys.
MAXLEN ~ whole nodes a few extra MAXLEN = exact edge cut precisely

true selects ~ (the safe default): Valkey trims in whole macro-node boundaries, which is cheaper, may under-trim (the stream can briefly hold a few more than count), but never over-trims — it can never delete an entry inside the window. false selects =: trim exactly to the window edge, at a higher cost. Either way the blast radius is bounded by the window, and trim/4 answers {:ok, removed_count} — the integer XTRIM returns. The ~ form is the right default precisely because the goal is a bound, not an exact count.

the pattern

Bound a structure so it cannot leak: LTRIM list 0 N keeps a List to its newest N; XTRIM…MAXLEN ~ keeps a Stream to roughly its newest N, trading exactness for a cheaper trim.

its EchoMQ application

The per-group wake list is LTRIM KEYS[7], 0, 63-capped at 64 (lanes.ex); the streams are XTRIM…MAXLEN ~-capped (stream.ex, with ~ the safe default). The memory ceiling is fixed in the code that writes the structure.

The cheapest guarantee against a memory leak is a cap at the write. EchoMQ caps the wake list at 64 and trims its streams with the approximate MAXLEN ~, so neither structure can grow without bound — exactness given up only where a bound, not a precise size, is what matters.

R7.01 · in this course

The same record, under noeviction

A cap stops a structure leaking; noeviction governs what happens when memory is exhausted anyway — the write fails loudly rather than a key vanishing. Redis as a primary database grounds that posture in the same job HASH this module keeps compact.

/bcs/fly

The maxmemory 512mb guardrail

Caps keep individual structures bounded; maxmemory 512mb is the loud ceiling on the whole keyspace. The committed config and its production posture — Valkey on a Fly machine — is the Branded Component System production chapter.

§4

References

sources · related

Sources

Related in this course