Redis 📂 Core data structures · 1 of 2 25 min read

Redis Data Structures — Strings, Counters, Lists, and Hashes

A practical Redis data-structures tutorial. Learn string counters with INCR, DECR and APPEND, lists for queues and recent-item caches with LPUSH, RPUSH, LRANGE and LTRIM, and hashes for storing objects like a user profile with HSET and HGETALL. Includes animated diagrams and a guide on when to use each structure.

Section 01

More Than Just Strings

The Right Container for the Job
In a kitchen you do not keep everything in one jar. Sugar goes in a jar, spoons go in a drawer, and a recipe card holds many labelled fields. Each container fits its contents.

Redis works the same way. A plain value is a string. An ordered line of items is a list. A small object with named fields is a hash. Picking the right structure makes your commands simpler and your app faster. This tutorial covers the three you will use most.
📜
String
one value / a counter
A single value under a key. Text, a number, or JSON. Great for caching one thing and for fast counters with INCR.
📋
List
an ordered line
Many items in order. Push to either end. Perfect for queues and for keeping the most recent items, like the last 50 chat messages.
📑
Hash
an object with fields
One key holds many named fields. Ideal for storing an object like a user profile without making a separate key for each field.

Section 02

Strings and Counters

A string is the simplest value. But a string that holds a number becomes a fast, safe counter. INCR adds one, DECR takes one away, and APPEND adds text to the end. These run in memory, so they are extremely quick.

# a plain string value
SET greeting "hello"
GET greeting

# a counter: start at 0, then add or subtract
SET page:views 0
INCR page:views          # -> 1
INCR page:views          # -> 2
INCRBY page:views 10      # -> 12  (add 10 at once)
DECR page:views          # -> 11

# APPEND adds text to the end of a string
SET log "line1;"
APPEND log "line2;"       # value is now "line1;line2;"
GET log
OUTPUT
SET page:views 0 -> OK INCR page:views -> (integer) 1 INCR page:views -> (integer) 2 INCRBY page:views 10-> (integer) 12 DECR page:views -> (integer) 11 APPEND log "line2;" -> (integer) 12 (new length)

Many app servers can hit the same counter at once. Because Redis runs one command at a time, every INCR is counted. No two updates ever clash.

🔢 One Safe Counter, Many Writers
INCR +1 INCR +1 INCR +1 app server A app server B app server C page:views (string counter) 98,214 every +1 is counted, none are lost
🧠
Why INCR Beats "read, add one, write back"

In an RDBMS, counting often means read the value, add one, and write it back. Two users at once can read the same number and one update is lost. INCR does the whole thing in one atomic step, so it is always correct and much faster.

CommandWhat it doesExample reply
INCR keyAdd 1 to the number(integer) 1
DECR keySubtract 1(integer) 0
INCRBY key nAdd any amount(integer) 10
DECRBY key nSubtract any amount(integer) 5
APPEND key textAdd text to the endnew length
💡
Great Uses for String Counters

Page views, likes, downloads, "items in stock", and rate limiting (count requests per minute, then let the key expire). Any number that many users change at once is a good fit.


Section 03

Lists — Ordered Lines of Items

A list keeps items in order. You can add or remove from either end. The left end is the head; the right end is the tail. LPUSH adds on the left, RPUSH adds on the right, and LPOP / RPOP remove from those ends.

Positions are numbered from 0 at the head. The last item is also -1, the one before it -2, and so on.

📋 Anatomy of a Redis List
"m5" "m4" "m3" "m2" "m1" 0 1 2 3 4 (also -1) head tail LPUSH (add left) LPOP (take left) RPUSH (add right) RPOP (take right)
# build a list from the right
RPUSH tasks "a" "b" "c"     # list is now: a, b, c

# read the whole list (0 to -1 means start to end)
LRANGE tasks 0 -1            # -> "a" "b" "c"

# add on the left
LPUSH tasks "z"              # list is now: z, a, b, c

# remove from an end
LPOP tasks                   # -> "z"   (list: a, b, c)
RPOP tasks                   # -> "c"   (list: a, b)

# how many items?
LLEN tasks                   # -> 2

Section 04

Lists as a Queue

Put items in one end and take them out of the other, and a list becomes a queue. A common pattern is a background job queue: producers LPUSH jobs on the left, and a worker RPOPs them from the right. First in, first out (FIFO).

01
Producer adds a job
Your app pushes work on the left: LPUSH jobs "send-email:57".
02
Jobs wait in order
The list holds jobs in the order they arrived. The oldest sits at the tail.
03
Worker takes the oldest
A worker pulls from the right: RPOP jobs (or BRPOP to wait for one). FIFO order.
# producer side: add jobs on the left
LPUSH jobs "send-email:57"
LPUSH jobs "resize-image:88"

# worker side: take the oldest job from the right (FIFO)
RPOP jobs                    # -> "send-email:57"

# BRPOP waits up to 5 seconds if the queue is empty
BRPOP jobs 5

Section 05

Lists for Recent Items (LTRIM)

Lists are perfect for "the latest N things": recent chat messages, a notification feed, or the last searches. The trick is LTRIM. After each push, trim the list so it keeps only the newest items and drops the old ones.

A new message is pushed to the head. LTRIM keeps the newest few, so the oldest item falls off the tail. The list never grows without limit.

✂️ Keep Only the Newest with LTRIM
LPUSH chat:42 "new" "new" "m3" "m2" "m1" "old" dropped newest (head) oldest (tail) LTRIM chat:42 0 3 → keep the newest 4, drop the rest
# cache the newest chat messages, keep only the last 50
LPUSH chat:42 "{id:9001, body:'hi'}"
LTRIM chat:42 0 49          # keep positions 0..49 (newest 50)

# read the newest 10 to show on screen
LRANGE chat:42 0 9
✅
LPUSH + LTRIM = a Self-Cleaning Cache

Push the newest item, then trim. The list stays a fixed size forever, so it never eats your memory. This is exactly how a "recent messages" or "recent activity" cache is built.

CommandWhat it does
LPUSH / RPUSH key vAdd an item on the left / right
LPOP / RPOP keyRemove and return the left / right item
LRANGE key start stopRead a range (0 -1 = all)
LTRIM key start stopKeep only that range, drop the rest
LLEN keyCount the items

Section 06

Hashes — Storing Objects

A hash is a key that holds many named fields. It is the natural way to store an object like a user profile. One key, many fields, read together or one at a time.

📑 A Hash Holds an Object Under One Key
HSET user:1 ... HGETALL user:1 Your app reads / writes one profile Hash key: user:1 FIELD VALUE name"Asha" email"asha@example.com" age29 city"Baddi" read one field with HGET, or all fields with HGETALL
# store a whole profile under one key, many fields at once
HSET user:1 name "Asha" email "asha@example.com" age 29 city "Baddi"

# read one field
HGET user:1 email          # -> "asha@example.com"

# read the whole object
HGETALL user:1             # -> name Asha email ... age 29 city Baddi

# update just one field, or bump a number field
HSET user:1 city "Shimla"
HINCRBY user:1 age 1       # age -> 30

# remove a field, or check if one exists
HDEL user:1 city
HEXISTS user:1 email       # -> 1
OUTPUT of HGETALL user:1
1) "name" 2) "Asha" 3) "email" 4) "asha@example.com" 5) "age" 6) "29" 7) "city" 8) "Baddi"

One hash vs many string keys

❌ Many string keys
user:1:name = "Asha"
user:1:email = "asha@..."
user:1:age = 29
4 keys, 4 round trips to read
✅ One hash
user:1 → {name, email, age, city}
1 key, tidy and grouped
read all with one HGETALL
uses less memory
💰
Hashes Save Memory

A small hash is stored in a compact way inside Redis, so one hash of ten fields uses less memory than ten separate string keys. For objects like profiles, carts, and settings, a hash is both tidier and lighter.


Section 07

Which Structure Should You Use?

📜
Use a String when
You store one value per key, or you need a fast counter. Cache a rendered page, a token, a JSON blob, or count views and likes with INCR.
SET, GET, INCR, APPEND
📋
Use a List when
Order matters and you add or remove from the ends. Job queues, recent messages, activity feeds. Cap the size with LTRIM.
LPUSH, RPUSH, LRANGE, LTRIM
📑
Use a Hash when
You store an object with named fields and often read or update single fields. User profiles, product details, settings, shopping carts.
HSET, HGET, HGETALL

Section 08

Golden Rules

🧩 Redis Data Structures — Non-Negotiable Rules
1
Match the structure to the data. One value or a count is a string. An ordered line is a list. An object with fields is a hash.
2
Count with INCR, not read-modify-write. INCR is one atomic step, so it is always correct even when many writers hit the same key.
3
Know your list ends. Left is the head, right is the tail. Position 0 is the first item and -1 is the last.
4
Cap lists with LTRIM. After each push, trim to the newest N. A recent-items list should never grow without a limit.
5
Store objects as hashes. One hash beats many separate keys: it is tidier, needs fewer round trips, and uses less memory.
6
Give every cache key a TTL. Strings, lists, and hashes can all expire. Set a time to live so old data clears itself.