skip to content

You want a Redis key holding only the 100 most recent events for each user, without it growing forever. Show how LPUSH together with LTRIM implements that, and explain what the trim actually costs and why the two commands should be issued together.

level: middleimportance: should knowfreq 42%

answer

  1. LPUSH + LTRIM key 0 99 in MULTI/EXEC
  2. LTRIM keeps the range, discards the rest
  3. O(N) in elements REMOVED, not list length
  4. trim every write => ~1 element, cheap
  5. caps count not bytes; emptying deletes the key

basics

~20 s

Push then trim: LPUSH feed:42 event, then LTRIM feed:42 0 99, which keeps only that index range and discards the rest. Send both in one MULTI/EXEC or Lua script so the list is never read oversized and the trim cannot be skipped. LTRIM is O(N) in elements removed, so trimming on every push removes about one element.

solid answer

~50 s

The capped-list idiom is two commands. LPUSH feed:42 <event> puts the newest item at the head, then LTRIM feed:42 0 99 keeps indexes 0 through 99 inclusive and deletes everything else. Reads are LRANGE feed:42 0 9 for the newest ten, already newest-first, and the key can never exceed 100 entries. Issue them as one unit — MULTI/EXEC or a Lua script — so no reader sees 101 entries and no crash between the two leaves the list untrimmed and growing. A pipeline only saves round trips; it gives no atomicity, and other clients' commands can interleave. LTRIM is O(N) where N is the number of elements it removes, not the list length. Trimming on every push removes about one element and is effectively constant. Trimming rarely, or shrinking an already huge list to a small cap, is a single expensive command that blocks other work while it runs.

code

text · 7 lines
text
MULTI
LPUSH feed:42 "login:2026-08-14T10:02Z"
LTRIM feed:42 0 99
EXEC

LRANGE feed:42 0 9   # ten newest, newest first
LLEN feed:42         # never exceeds 100

go deeper

for a junior

Know the LPUSH plus LTRIM key 0 99 idiom and that LTRIM keeps the given range and drops the rest.

for a middle

Explain the atomicity requirement, the index conventions for LPUSH versus RPUSH, and that LTRIM cost scales with elements removed.

for a senior

Discuss the latency impact of large trims, stepwise trimming, UNLINK for disposal, and byte-versus-count capping.

for a principal

Set the retention policy: cap sizes across millions of keys, memory budget per key family, and when a list-shaped feed should become a different structure entirely.

## The idiom Redis has no built-in maximum length for a List, so bounded structures are built by convention: push the new element at one end and trim the far end away. LPUSH feed:42 event; LTRIM feed:42 0 99 LTRIM key start stop is subtractive: it keeps the specified inclusive index range and removes everything outside it. Indexes are zero-based and negatives count from the end, so LTRIM key 0 99 keeps the hundred newest when pushing with LPUSH, while LTRIM key -100 -1 keeps the hundred newest when pushing with RPUSH. Out-of-range values are clamped, so trimming a list shorter than the cap is a harmless no-op, and a range selecting nothing empties the list, deleting the key. This gives a fixed memory bound per key: cap times average element size. For a timeline of a hundred short events that is a few kilobytes, so millions of user feeds become a predictable budget instead of unbounded growth. ## Why the pair must be atomic Between LPUSH and LTRIM the list is momentarily over the cap, and if the client dies in that gap the list stays over the cap until the next push. One extra element is harmless, but the failure compounds if trims are systematically skipped. Wrapping the pair in MULTI/EXEC makes them execute as one unit, so no other client observes the intermediate state and neither command can happen without the other. A pipeline is not a substitute: it batches round trips, but the server may interleave other clients' commands between them, and a disconnect mid-pipeline can leave only the push applied. A tiny Lua script gives the same atomicity as MULTI/EXEC and is convenient when the cap is computed. ## What the trim costs The documented complexity is O(N) with N the number of elements removed. This is the number most candidates get wrong, and the consequences follow directly. Trimming after every push means each LTRIM removes at most one element, so the amortised cost is constant and it is cheap enough for the write path. Trimming lazily — every hundredth push, or in a nightly job — saves a command per write but concentrates the work: a single LTRIM removing 100,000 elements is a long-running command, and while it runs no other command is served, since Redis executes commands one at a time. The same trap appears when a cap is reduced: shrinking a million-element list to 100 in one LTRIM is a large synchronous deletion. The safe form when a list is already huge is to trim in steps — repeated LTRIMs each removing a bounded number of elements — or, if the list is disposable, UNLINK the key, which reclaims memory in a background thread instead of inline. ## Related pitfalls A capped list bounds count, not bytes. If elements vary wildly in size, a hundred entries can still be megabytes; cap by count and enforce a size limit on payloads, or store large bodies elsewhere and keep only ids in the list. Remember also that a List permits duplicates and does not deduplicate, so a capped list is a log of recent events, not a set of distinct ones. And if a trim ever empties the list the key is deleted, taking any TTL with it — so a per-key expiry must be re-applied by the writer rather than assumed to persist.

  • Is it cheaper to trim once every thousand pushes instead of on every push?
    Not in any way that helps. You save round trips, but the eventual LTRIM must remove around a thousand elements in one command, and Redis runs it to completion before serving anyone else. Trimming on each write keeps every trim to roughly one element and avoids the latency spike.
  • How would you shrink an existing ten-million-element list down to 1000 safely?
    Not with a single LTRIM, which would delete nearly ten million elements in one blocking command. Trim in bounded steps, each removing a manageable slice, or if the contents are disposable, UNLINK the key so memory is reclaimed by a background thread and rebuild it.

saying these in an interview costs you the question

  • Believing LTRIM's cost scales with the surviving elements rather than the removed ones
  • Sending LPUSH and LTRIM as unrelated commands and calling the result atomic
  • Treating a pipeline as equivalent to MULTI/EXEC for atomicity
  • Batching trims to save work, creating one long blocking command
  • Assuming a capped list bounds memory in bytes regardless of element size

context