Redis Data Types Explained: Strings, Hashes, Lists, Sets
AI generated
SET
TTL
Redis · Data Structures · In-Memory · Performance
Redis Data Types Explained
Strings, hashes, lists and sets done right

Redis is far more than a simple key value store for strings. Anyone who understands the four core data types, strings, hashes, lists and sets, and their internal memory encoding can build data models that use noticeably less memory and respond significantly faster than a naive implementation that leans on a single data type.

17 min read Strings · Hashes · Lists · Sets · Memory Encoding Redis 6.x · 7.x

1. Why Redis data types are more than syntax

Anyone using Redis for the first time tends to treat it as a glorified key value store: a string as the value, done. That works, but it gives away the biggest advantage Redis offers. The real strength lies in the Redis data types themselves: strings, hashes, lists, sets and sorted sets are not plain serialization formats, they are native structures with their own commands, their own memory layout and their own complexity guarantees. Anyone storing a complex object as a JSON string must read the whole value, deserialize it, change it and write the entire thing back for every tiny update.

With the matching Redis data type, that detour disappears entirely. A single field of a hash can be changed with HSET without touching the remaining fields. An element of a list can be appended with LPUSH without re-serializing the whole list. These operations are atomic, run server side and save network round trips. Anyone who consciously chooses among the Redis data types reduces not just memory usage but also the number of commands an application sends to Redis per request.

This article walks through the four most common Redis data types in detail: strings, hashes, lists and sets. Sorted sets, HyperLogLog, bitmaps and streams are their own, more specialized structures and are covered in separate posts. The focus here is on the commands, the internal memory encoding and the criteria for choosing between structures.

2. Strings: more than a simple cache value

The string is the simplest of the Redis data types, but far from the most trivial. A Redis string can be up to 512 MB in size and is binary safe, meaning it can hold arbitrary bytes, not just text. Beyond SET and GET, Redis offers atomic counting operations such as INCR, INCRBY and DECRBY that work without race conditions even when hundreds of clients access them concurrently. That makes strings the natural choice for counters such as page views, rate limits or stock levels.

An often overlooked feature is combining SET directly with a time to live via the EX or PX option, which sets a value and its expiry in a single atomic command. That is the foundation for session storage, verification codes and temporary locks. SETNX, or the more modern form SET key value NX, sets a value only if the key does not exist yet, making it the basis for distributed locks. GETRANGE and SETRANGE allow partial operations on the string without transferring the whole value.

For binary data such as bitmaps or serialized protobuf messages, the string type is the underlying foundation too, even though specialized commands such as SETBIT build on top of it. In practice, the string is the Redis data type with the highest usage share because it is sufficient for caching scenarios with simple values, counters and flags and requires no additional modeling.


# Strings: counters, TTL-based sessions, atomic locks
redis-cli SET pageviews:home 0
redis-cli INCR pageviews:home
redis-cli INCRBY pageviews:home 42
redis-cli SET session:abc123 "active" EX 3600
redis-cli TTL session:abc123
redis-cli SET lock:invoice:9001 "worker-1" NX EX 30
redis-cli GETRANGE session:abc123 0 2
redis-cli STRLEN session:abc123
redis-cli APPEND pageviews:home ":verified"

3. Hashes: structured objects without overhead

A hash maps fields to values within a single key and is therefore the natural Redis data type for objects such as user profiles, product data or configuration sets. Instead of creating a separate string key for every field, such as user:1000:name and user:1000:email, a hash bundles all fields under a single top level key user:1000. That drastically reduces the number of keys in the keyspace table, which directly saves memory because every top level key in Redis carries its own management overhead.

With HSET, single or multiple fields can be set at once, with HGET and HMGET individual or multiple fields can be read without transferring the whole object. HGETALL returns the entire object but should be used carefully on very large hashes because it is an O(n) operation. HINCRBY allows atomic counting on individual fields, for example a login counter inside a user profile, without needing a separate counter key.

A common modeling mistake is storing a whole object as a JSON string in a single Redis key, even though a hash would be the more natural and efficient structure. The difference shows up with partial updates: with a JSON string the entire string has to be read, parsed client side, one field changed and the complete string written back. With a hash, a single HSET user:1000 last_login 1721742000 call suffices, running server side and atomically.


# Hashes: structured objects with partial field access
redis-cli HSET user:1000 name "Max Mustermann" email "max@example.com" plan "pro"
redis-cli HGET user:1000 email
redis-cli HMGET user:1000 name plan
redis-cli HGETALL user:1000
redis-cli HINCRBY user:1000 login_count 1
redis-cli HDEL user:1000 plan
redis-cli HEXISTS user:1000 email
redis-cli HLEN user:1000

4. Lists: queues and activity feeds

Redis lists are doubly linked lists and therefore the right Redis data type for ordered sequences where inserting and removing at both ends must be fast. LPUSH inserts elements at the head, RPUSH at the tail, both operations run in constant time O(1) regardless of list length. That property makes lists the standard choice for simple job queues: producers append tasks with LPUSH, workers pick them up with RPOP, the result is a first in first out queue without any extra infrastructure.

For blocking waits on new elements, Redis offers BRPOP and BLPOP, which block a client until an element becomes available or a timeout expires. That replaces polling loops that would otherwise burn CPU and network bandwidth unnecessarily. For reliable processing with recovery after a worker crash there is LMOVE, which atomically moves an element from a source list to a destination list, so an element stays visible in a processing list while it is being handled.

Besides queues, lists are suited for bounded activity feeds: LPUSH followed by LTRIM caps a list at a fixed length, for example the last 100 events. For random access to a single element in the middle of a very long list, lists are the wrong choice though, because LINDEX in the middle of the list has to traverse linearly and therefore costs O(n), while both ends remain reachable in constant time.


# Lists: job queues and bounded activity feeds
redis-cli LPUSH queue:emails "job:4821"
redis-cli LPUSH queue:emails "job:4822"
redis-cli RPOP queue:emails
redis-cli BRPOP queue:emails 5
redis-cli LMOVE queue:emails queue:emails:processing LEFT RIGHT
redis-cli LPUSH feed:user:1000 "liked photo:9981"
redis-cli LTRIM feed:user:1000 0 99
redis-cli LRANGE feed:user:1000 0 9
redis-cli LLEN queue:emails

5. Sets: membership and set operations

A set stores an unordered collection of unique strings and answers one question especially efficiently: is this element part of the collection? SISMEMBER runs in constant time O(1) regardless of how many elements the set holds. That makes the set data type the right choice for tag systems, permission lists or deduplicating events, for example checking whether a user ID has already participated in an action.

The real strength of sets shows up with set operations: SINTER computes the intersection of multiple sets, SUNION the union, SDIFF the difference. These operations run entirely server side without the client having to load elements individually and compare them itself. A typical example: friend recommendations via the intersection of two users' follower sets, or finding products tagged with both tag A and tag B via SINTERSTORE into a new, reusable set.

For very large collections, SRANDMEMBER offers a random sample without removing the element, while SPOP removes and returns a random element, handy for lottery mechanics or pulling test candidates from a pool. Important to understand: sets guarantee no ordering. Anyone needing a sorted member list, for example by score or timestamp, should switch to the sorted set data type, which is covered in a separate post.


# Sets: membership checks and server-side set operations
redis-cli SADD tags:article:501 "redis" "caching" "performance"
redis-cli SISMEMBER tags:article:501 "redis"
redis-cli SADD followers:userA "u1" "u2" "u3"
redis-cli SADD followers:userB "u2" "u3" "u4"
redis-cli SINTER followers:userA followers:userB
redis-cli SUNIONSTORE followers:combined followers:userA followers:userB
redis-cli SCARD followers:combined
redis-cli SPOP giveaway:candidates 3

6. Memory encoding: listpack, intset and thresholds

Something many developers do not know: Redis stores small instances of every data type internally in a more compact form to save memory. That compact form is called listpack (called ziplist in older versions) and is used automatically as long as a hash, list or set stays below certain size thresholds. A set consisting purely of integers gets encoded even more specifically as an intset, a sorted array of integers that needs even less memory than a listpack.

The relevant configuration values are called hash-max-listpack-entries, hash-max-listpack-value, list-max-listpack-size, set-max-listpack-entries and set-max-intset-entries. Once an object exceeds one of these thresholds, Redis converts it automatically and transparently to the full structure, for hashes and sets typically a hash table, for lists a quicklist made of several listpack nodes. This conversion is one directional: an object that has been converted once does not automatically shrink back to the compact form, even if elements are removed again.

With OBJECT ENCODING, the current internal representation of any key can be inspected at any time. This is especially relevant for capacity planning: a hash with thousands of small objects and few fields per object stays in listpack encoding and needs noticeably less memory than an equivalent set of individual string keys. Anyone who knows the default thresholds and deliberately models below them, for example by sharding large hashes into several smaller ones, can significantly reduce memory usage.


# redis.conf: memory-encoding thresholds for compact structures
# English comments describe the effect of each threshold

# Hash switches from listpack to hashtable above these limits
hash-max-listpack-entries 128
hash-max-listpack-value 64

# List nodes switch from listpack to quicklist above this size
list-max-listpack-size 128

# Set switches from listpack/intset to hashtable above these limits
set-max-listpack-entries 128
set-max-intset-entries 512

# Inspect the current encoding of a live key
redis-cli OBJECT ENCODING user:1000
redis-cli OBJECT ENCODING tags:article:501
redis-cli OBJECT ENCODING queue:emails

7. Choosing the right structure for the use case

Choosing between the Redis data types should always start from the access pattern, not from what seems like the easiest path. The guiding question is: how is the data accessed? If the application needs a single field of an object without loading the whole object, a hash is almost always the better choice than a serialized string. If order must be preserved and fast insertion is needed at both ends, a list is the right structure. If it is only about membership or set operations between two collections, a set is the natural choice.

Another criterion is expected size. Small objects with few fields benefit strongly from listpack encoding and should be modeled to stay below the default thresholds. Very large collections with tens of thousands of elements should instead be deliberately sharded, for example by splitting a huge hash into several smaller hashes using a hash modulo scheme, to keep the latency of individual operations low and avoid burdening replication with a single, enormous key.

A third, often underestimated factor is the need for atomic partial operations. When multiple clients access the same object concurrently and only change individual fields, a hash with targeted HSET calls prevents race conditions that would otherwise arise from a read modify write cycle on a serialized string object. These three criteria, access pattern, expected size and atomicity requirements, together form a reliable checklist for choosing the appropriate Redis data type.

8. Complexity classes and performance in practice

Every Redis command is documented with a Big O complexity class, and that annotation is not an academic footnote but decisive for production performance. HGET, LPUSH, SADD and SISMEMBER all run in O(1), regardless of the size of the underlying structure. HGETALL, LRANGE over an entire list and SMEMBERS run in O(n) instead and can block Redis's single threaded event loop for several milliseconds on very large collections, during which no other request gets processed.

Commands like KEYS or a careless SMEMBERS on a set with millions of elements are especially dangerous in high throughput production environments. The recommendation is to consistently use the cursor based scan variants SCAN, HSCAN, SSCAN and LRANGE with a bounded offset for large collections, which split iteration into small, non-blocking steps. These commands return only a slice per call plus a cursor for the next call, so the event loop stays free for other clients between steps.

9. Redis data types compared

The table below summarizes the four Redis data types discussed here with their typical use case, their most relevant complexity class and the applicable memory encoding, to make the decision easier for a concrete project.

Data Type Typical Use Case Core Complexity Compact Encoding
String Counters, sessions, locks O(1) int / embstr / raw
Hash Objects, profiles, partial updates O(1) per field listpack
List Queues, feeds, log buffers O(1) at ends listpack / quicklist
Set Membership, set logic O(1) member test intset / listpack

The table shows that all four Redis data types offer constant time for their core operations, as long as the O(n) traps such as HGETALL or SMEMBERS on large collections are avoided in favor of scan based iteration. Compact encoding is an automatic mechanism for all four types, kicking in without any extra work as long as the configured size thresholds are not exceeded.

Mironsoft

Redis architecture, caching strategies and Magento performance

Is your Redis data model not tailored to the use case yet?

We analyze existing Redis keyspaces, identify inefficient modeling and build data structures that save memory and cut latency, including Magento cache backends.

Keyspace Analysis

Identify memory encoding, key sizes and O(n) traps

Data Model Redesign

Hashes, lists and sets instead of serialized JSON strings

Magento Integration

Set up and tune Redis as a cache and session backend for Magento 2

10. Summary

The four core Redis data types, strings, hashes, lists and sets, each solve a different modeling problem. Strings suit counters, sessions and simple values with an optional TTL. Hashes bundle related fields under one key and allow atomic partial updates without the overhead of separate string keys. Lists provide ordered sequences with constant time at both ends and form the basis for simple queues. Sets answer membership questions in constant time and offer server side set operations such as intersection and union.

The internal memory encoding with listpack and intset ensures that small instances of these Redis data types are automatically stored compactly, as long as configured thresholds are not exceeded. Anyone who knows these thresholds and factors them into modeling can significantly cut memory usage without sacrificing functionality. Choosing the right structure should always start from the access pattern, not from what looks like the simplest implementation using a serialized string.

Redis Data Types Explained: The Essentials at a Glance

Strings

Atomic counters with INCR, TTL based sessions with SET ... EX, locks with SET NX.

Hashes

Objects with partial updates via HSET, no read modify write on serialized strings needed.

Lists & Sets

Lists for queues with O(1) at both ends, sets for membership and set operations in O(1).

Memory Encoding

listpack and intset automatically save memory below the configured thresholds.

11. FAQ: Redis Data Types Explained

1Hash instead of JSON string?
Whenever fields change independently. HSET allows atomic partial updates without read modify write.
2Why is HGETALL risky?
O(n) and blocks the event loop. Use HSCAN with a cursor for large hashes instead.
3listpack vs. intset?
listpack is a compact format for mixed small structures. intset is even more compact but only for pure integer sets.
4Check a key's encoding?
OBJECT ENCODING keyname shows listpack, intset, hashtable or quicklist depending on size and content.
5Does a hash convert back?
No, the conversion is one directional. An object stays in the full encoding even after fields are deleted.
6Lists for large queues?
Suitable for simple FIFO queues. For consumer groups and retry logic, Streams are the better choice.
7Compare sets efficiently?
SINTER, SDIFF and SUNION run server side, without the client comparing elements individually.
8How many fields per hash?
Under hash-max-listpack-entries memory stays low. For very large objects, shard into several hashes.
9Why is KEYS dangerous?
KEYS blocks Redis entirely in O(n). SCAN with a cursor splits iteration into non-blocking steps.
10TTL on hash fields?
Possible since Redis 7.4 with HEXPIRE. Before that, TTL applied only to the whole key, not individual fields.