LINDEX, LSET, and LLEN

Redis lists are ordered collections of strings, and three commands let you interact with individual elements and the list as a whole without pulling the entire list into your application: LINDEX reads a single element by its position, LSET overwrites a single element in place, and LLEN reports how many elements the list currently holds. Together they give you random access into a data structure that is otherwise optimized for pushing and popping from the ends.

Overview / How it works

Internally, a Redis list is implemented as a listpack (a compact, contiguous encoding used for small lists) that Redis automatically converts to a quicklist — a doubly linked list of listpack nodes — once the list grows past certain size or element-length thresholds. This matters directly for LINDEX and LSET: neither of these commands can jump straight to an arbitrary offset the way an array in memory would. Redis has to walk the list from whichever end is closer (the head if the index is small and positive, the tail if the index is small and negative) to reach the target position. That walk is why both commands are documented as O(N) in the general case, even though accessing the first or last element is effectively O(1) because no traversal is needed.

LLEN, by contrast, is O(1) — Redis maintains the element count for a list as metadata, so it never has to walk the list to answer "how many elements are there." This asymmetry — cheap length checks, potentially expensive middle-of-list access — is a good mental model for how Redis lists behave at scale: they’re excellent for queues, stacks, and recent-activity feeds where you mostly touch the ends, and less ideal as a substitute for an indexable array you plan to randomly access deep into.

Indexing works like most zero-based sequences: index 0 is the head (leftmost, oldest-pushed-via-RPUSH) element, positive indices count forward from the head, and negative indices count backward from the tail, where -1 is the last element, -2 the second-to-last, and so on. This mirrors Python-style negative indexing and is consistent across every Redis list command that accepts an index or range.

Syntax

LLEN key
LINDEX key index
LSET key index element
Command Arguments Time Complexity
LLEN key — the list key O(1)
LINDEX key — the list key; index — zero-based position, negative counts from the tail O(N) where N is the number of elements traversed to reach index (O(1) at either end)
LSET key — the list key; index — position to overwrite; element — the new value O(N), also O(1) at either end
  • LLEN returns the integer number of elements in the list, or 0 if the key doesn’t exist.
  • LINDEX returns the element at index as a bulk string, or (nil) if the index is out of range or the key doesn’t exist. It never errors just because the index is too large — an out-of-range read is a normal, expected outcome.
  • LSET replaces the element at index and returns OK. Unlike LINDEX, it does error if the index is out of range or the key doesn’t exist, because there’s no sensible way to "set" a position that isn’t there.

Examples

Example 1: Basic reads with LLEN and LINDEX

RPUSH playlist:1 "Bohemian Rhapsody" "Stairway to Heaven" "Hotel California"
LLEN playlist:1
LINDEX playlist:1 0
LINDEX playlist:1 2
LINDEX playlist:1 -1
LINDEX playlist:1 5

Output:

(integer) 3
(integer) 3
"Bohemian Rhapsody"
"Hotel California"
"Hotel California"
(nil)

After pushing three songs, LLEN confirms three elements. LINDEX playlist:1 0 reads the head, LINDEX playlist:1 2 reads the last element by its positive index, and LINDEX playlist:1 -1 reaches the same last element from the tail end — both return "Hotel California". Asking for index 5, which doesn’t exist, simply returns (nil) rather than an error.

Example 2: Updating an element with LSET

RPUSH queue:tasks "task1" "task2" "task3"
LSET queue:tasks 1 "task2-updated"
LINDEX queue:tasks 1

Output:

(integer) 3
OK
"task2-updated"

The list starts with three tasks. LSET queue:tasks 1 "task2-updated" overwrites the middle element in place — the list stays the same length, only that one slot’s value changes — and the follow-up LINDEX confirms the replacement stuck.

Example 3: Negative indexing in a realistic scenario

RPUSH playlist:2 "song-a" "song-b" "song-c"
LSET playlist:2 -1 "song-c-remix"
LINDEX playlist:2 -1
LLEN playlist:2

Output:

(integer) 3
OK
"song-c-remix"
(integer) 3

This is a common pattern: swap out the most recently added item (the tail) without knowing its exact positive index or the list’s current length. LSET playlist:2 -1 ... always targets the last element regardless of how long the list is, and LLEN shows the length is unchanged — LSET never adds or removes elements, it only replaces one.

How it works step by step

LLEN playlist:missing
LINDEX playlist:missing 0

Output:

(integer) 0
(nil)

When you run LINDEX key index, Redis first looks up key in the main keyspace dictionary. If the key doesn’t exist, it returns (nil) immediately — as shown above — with no error, since a missing key is treated as an empty list for read purposes. If the key exists and holds a list, Redis normalizes a negative index (index -1 becomes length - 1), checks it’s within bounds, and then traverses the underlying quicklist nodes from whichever end is nearer until it reaches the target position, reading that element’s bytes. LSET follows the same lookup and traversal, but instead of reading it replaces the string at that position and, because a list is a single-threaded, atomic structure in Redis, no other command can observe the list half-updated — the write is all-or-nothing from any other client’s point of view. LLEN skips traversal entirely: every list keeps a running element count, so it’s a single field read regardless of list size.

Common Mistakes

Mistake 1: Assuming LSET on an out-of-range index just extends the list. Unlike some languages’ array assignment, LSET will not grow a list to accommodate an index beyond its current bounds — it errors instead.

RPUSH playlist:3 "only-song"
LSET playlist:3 5 "new-song"

Output:

(integer) 1
(error) ERR index out of range

To add elements, use RPUSH or LPUSH; reserve LSET strictly for overwriting a position you know already exists.

Mistake 2: Calling LSET on a key that doesn’t exist yet. Because LSET requires an existing list to modify, it errors on a missing key instead of silently creating one — this trips people up who expect "set" semantics like a plain SET.

LSET playlist:missing 0 "value"

Output:

(error) ERR no such key

Push at least one element with RPUSH/LPUSH first, or check LLEN is greater than zero, before calling LSET.

Mistake 3: Running list commands against a key that isn’t a list. Every Redis key has exactly one data type, and LINDEX/LSET/LLEN only work on keys created with list commands.

SET user:1001:name "Ada"
LINDEX user:1001:name 0

Output:

OK
(error) WRONGTYPE Operation against a key holding the wrong kind of value

If you hit WRONGTYPE, use TYPE key to check what the key actually holds before assuming your list command is correct.

Mistake 4: Using LINDEX in a loop to read every element. Because LINDEX is O(N) per call for elements away from the ends, reading a 10,000-element list one index at a time in application code costs roughly O(N²) total work. Use LRANGE key 0 -1 (covered in its own lesson) to fetch a contiguous range in one O(N) call instead.

Best Practices

  • Use LLEN freely — it’s O(1) and safe to call as often as you need, even in hot paths, to check whether a list is empty or how large it’s grown.
  • Prefer accessing the head (0) and tail (-1) with LINDEX over middle indices when possible — those reads stay close to O(1) since no traversal is needed.
  • Check LLEN (or catch the error) before calling LSET on an index you haven’t verified exists, since LSET errors rather than extending the list.
  • Don’t use LINDEX in a loop to dump a whole list — use LRANGE key 0 -1 for bulk reads instead.
  • Remember negative indices (-1, -2, …) count from the tail — they’re the idiomatic way to reach "the most recent item" without tracking the list’s length yourself.
  • If you find yourself needing frequent random access deep into a large list, reconsider the data structure — a Redis hash keyed by a stable ID, or a sorted set if you need ordering, is often a better fit than treating a list like an array.

Practice Exercises

  • Create a list recent:logins by pushing three usernames with RPUSH. Use LLEN to confirm the count, then use LINDEX with a negative index to read the most recently pushed username without knowing the list’s length in advance.
  • Push four items onto a list tasks:pending, then use LSET to mark the second item (index 1) as "in-progress" by overwriting its value. Verify the change with LINDEX, and confirm LLEN still reports four elements afterward.
  • Try calling LSET on an index one past the end of a short list you create, and separately on a key you never created. Predict the two error messages before running them, then compare against the actual replies.

Summary

  • LLEN key returns the element count in O(1), or 0 for a missing key.
  • LINDEX key index reads one element by position (0-based, negative from the tail) in O(N) worst case, returning (nil) for an out-of-range index or missing key rather than erroring.
  • LSET key index element overwrites one element in place in O(N) worst case, returning OK on success, but errors with index out of range or no such key if the target position doesn’t already exist.
  • Accessing either end of a list (index 0 or -1) is effectively O(1); accessing near the middle costs a real traversal — plan access patterns accordingly.
  • A WRONGTYPE error means the key exists but isn’t a list; use TYPE key to check before debugging further.
  • For reading many or all elements at once, use LRANGE rather than looping over LINDEX calls.