In-Memory Store

Redis

Redis is an in-memory data-structure server: keys map to strings, hashes, lists, sets, sorted sets and streams, served from RAM by a single-threaded command loop.

intermediate14 min readUpdated Sep 16, 2026
redis-cli
bash
# redis-cli
SET session:9f2c '{"userId":42}' EX 3600

HSET cart:42 sku:KB-01 1 sku:MS-02 2
ZADD leaderboard 1500 ada 1420 grace
ZREVRANGE leaderboard 0 9 WITHSCORES

INCR rate:203.0.113.7:2026091612
EXPIRE rate:203.0.113.7:2026091612 60
Released
2009
Data model
In-memory key-value
Written in
C
Data structures
Strings, hashes, lists, sets, sorted sets, streams
Persistence
RDB + AOF
License
AGPLv3 (Redis 8+)
Version
8.x

Why it matters

Why Redis is the default cache

Latency in microseconds

Because the dataset lives in RAM and commands are simple, reads and writes return in well under a millisecond on ordinary hardware.

Data structures, not just strings

Hashes, lists, sets, sorted sets and streams are first-class, so counters, queues and rankings are built in rather than layered on top.

Replication and clustering

Replicas, Sentinel failover and Redis Cluster let a single instance grow into a highly available, sharded deployment.

The big picture

The three ideas behind Redis

Everything is a key, the value has a data structure, and every command runs one at a time on a single thread.

Keys

Address

Every value is stored under a string key. A clear naming scheme is the closest thing Redis has to a schema.

TTL & eviction

Reclaim

Keys can expire after a time or a period of inactivity, and a memory policy decides what to evict when the instance is full.

Commands

Operate

Operations are small atomic commands such as GET, HSET and ZADD, composed from your application or grouped in a pipeline.

Data model

How keys and values are shaped

A Redis database is a map of string keys to typed values, chosen per use case.

Keys and the structures behind themKey-value map
  • session:{id}hashSession fields with a sliding TTL
  • rate:{ip}:{minute}stringCounter for a fixed-window rate limit
  • leaderboardsorted setMembers scored by points, ranked with ZREVRANGE
  • queue:emailslistLPUSH / BRPOP work queue
  • user:{id}:followerssetUnique follower ids with no duplicates
  • eventsstreamAppend-only log read with consumer groups

Redis stores every value under a string key; the value type is chosen per use case.

A short history

From a real-time logger to a data platform

  1. 2009

    Redis is created

    Salvatore Sanfilippo builds Redis to speed up a real-time analytics product, then open-sources it.

    09
  2. 2010

    VMware takes stewardship

    The project gains full-time maintainers, and Redis 2.0 adds hashes, pub/sub and replication.

    10
  3. 2012

    Lua scripting

    Redis 2.6 adds server-side Lua, letting several commands run atomically in one round trip.

    12
  4. 2015

    Redis Cluster ships

    Redis 3.0 introduces automatic sharding across nodes, along with a redesigned replication protocol.

    15
  5. 2018

    Streams arrive

    Redis 5.0 adds a log-like data structure with consumer groups, making it a capable message broker.

    18
  6. 2020

    Security and threads

    Redis 6 adds ACLs, TLS and threaded I/O, and RESP3 modernises the protocol.

    20
  7. 2025

    Redis 8 and AGPL

    After a licence change in 2024, Redis 8 returns to an open-source licence and bundles the former Stack modules.

    25

The complete guide

Redis: Everything you need to know

What is Redis?

Redis is an in-memory data-structure server. It keeps its entire dataset in RAM and answers commands in microseconds. Every value is stored under a string key, and the value can be a string, a hash, a list, a set, a sorted set, a stream, a bitmap or a HyperLogLog.

It began in 2009 as a way to speed up a real-time analytics product and quickly became the default cache for the web. The reason is not only speed. Redis also brings a small, composable command set, useful data structures, TTLs, replication and scripting — enough that teams use it for far more than caching.

Think of Redis as a shared, in-memory data structure that every process can see. That framing explains both its strengths and its limits.

Why Redis is fast

Three things make Redis fast, and none of them is a secret.

First, the data lives in memory. There is no disk seek and no buffer pool to manage on the read path. Memory access is measured in nanoseconds; a network round trip is measured in fractions of a millisecond.

Second, there is no query planner. A command such as GET user:42 is a hash-table lookup, and ZADD is a skip-list insert. There is no parsing of a query language, no optimisation step and no joins. The operation is fixed and direct.

Third, commands run one at a time on a single thread. That sounds like a weakness, but it removes lock contention and makes every command atomic. The event loop handles thousands of connections and executes commands sequentially, which is why a single instance can serve enormous throughput.

The catch is that one slow command blocks everything. KEYS * over millions of keys, or ZRANGE over a huge sorted set, stalls every other client. Keep commands bounded and use SCAN instead of KEYS in production.

The data structures that matter

Choosing the right structure is most of the skill. Here is what each one is for.

Strings hold text, JSON, counters and binary data. SET, GET, INCR, APPEND and SETEX are the workhorses.

SET user:42:name "Ada"
INCR page:home:views
SETEX token:abc123 900 "opaque-token-value"

Hashes store an object as field-value pairs under one key. They are perfect for sessions and records you update field by field, because you avoid serialising the whole object on every write.

HSET session:9f2c userId 42 role admin
HINCRBY session:9f2c pageViews 1
HGET session:9f2c role

Lists are ordered sequences with O(1) push and pop at both ends. They make simple queues, recent-activity feeds and capped logs.

LPUSH queue:emails "welcome:42"
RPOP queue:emails
LTRIM feed:global 0 99

Sets hold unique, unordered members. Use them for tags, followers and membership tests.

SADD post:7:tags redis database
SISMEMBER post:7:tags redis
SINTER user:1:follows user:2:follows

Sorted sets are the star. Every member has a score, so the set stays ordered by score while lookups stay O(log n). Leaderboards, priority queues and rate-limit windows all use them.

ZADD leaderboard 1500 ada
ZINCRBY leaderboard 50 ada
ZREVRANGE leaderboard 0 9 WITHSCORES

Streams are an append-only log with consumer groups, closer to Kafka than to a list. Use them when you need replay, acknowledgement and multiple consumers.

XADD events * type signup userId 42
XREADGROUP GROUP workers alice COUNT 10 STREAMS events ">"

Bitmaps and HyperLogLogs handle specialised counting. Bitmaps track per-user booleans in one bit each, ideal for daily active users; HyperLogLog estimates unique counts with about 12 KB and a small error rate.

SETBIT active:2026-09-16 42 1
PFADD visitors:2026-09-16 user:42
PFCOUNT visitors:2026-09-16

If you remember one rule, make it this: pick the structure that matches the access pattern, not the one that matches the JSON you already have.

Key naming and namespacing

Redis has no tables, no schemas and no namespaces. The only structure is the key itself, so a naming convention is your schema.

A common pattern is object:id:attribute in colon-separated segments:

user:42
user:42:sessions
session:9f2c
order:1001:items
rate:203.0.113.7:2026091612

Short, predictable keys keep memory down and make scanning and debugging easier. Add a version or environment prefix when several applications share an instance: app:v2:user:42. Avoid keys derived from unbounded user input, and never use spaces or newlines.

Because keys are strings, you can list them for debugging, but do it carefully. SCAN with a cursor is safe; KEYS is not, because it blocks the server while it walks the whole keyspace.

Expiry, TTL and eviction

Expiry is what makes Redis a cache rather than a leak. Almost every key you create for caching should have a TTL.

SET user:42 '{"name":"Ada"}' EX 300   # seconds
SET user:42 '{"name":"Ada"}' PX 300000 # milliseconds
EXPIRE user:42 300
TTL user:42
PERSIST user:42

EXPIRE and its variants attach a timeout; TTL reports the remaining seconds. Expiry is lazy and active: keys are removed when accessed after their time, and a background cycle samples and clears expired keys so memory is reclaimed even if they are never read again.

TTLs alone are not enough when the dataset grows faster than it expires. Set maxmemory and a maxmemory-policy:

  • noeviction — reject writes when full; the safe default for durable data.
  • allkeys-lru — evict the least recently used key of any kind; the common cache choice.
  • allkeys-lfu — evict the least frequently used key, better when a small hot set matters.
  • volatile-lru / volatile-ttl — evict only keys that have an expiry, so persistent keys are protected.
CONFIG SET maxmemory 2gb
CONFIG SET maxmemory-policy allkeys-lru

The choice matters. allkeys-lru will happily evict a session if it is the coldest key; volatile-ttl will not, because sessions always carry an expiry.

Caching patterns and invalidation

The most common pattern is cache-aside. The application checks Redis first, falls back to the database on a miss, and writes the result back with a TTL.

async function getUser(id) {
  const key = `user:${id}`;
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const user = await db.users.findById(id);
  if (user) await redis.set(key, JSON.stringify(user), { EX: 300 });
  return user;
}

Two other patterns are worth knowing. Write-through updates the cache and the database together, which keeps reads warm but doubles the write path. Write-behind writes to Redis first and flushes to the database asynchronously, which is fast but risks losing writes.

Invalidation is the hard part. A TTL guarantees eventual correctness; explicit deletes make it immediate. The reliable habit is to update the database first, then delete the cache key, and to accept a brief window where a concurrent reader repopulates stale data.

async function updateUser(id, patch) {
  const user = await db.users.update(id, patch);
  await redis.del(`user:${id}`);
  return user;
}

Avoid the temptation to keep a cache forever because invalidation is annoying. Stale data and unbounded memory are worse problems than a slightly lower hit rate.

Sessions, rate limiting and leaderboards

Redis shines wherever state must be shared across processes and survive restarts.

Sessions are a hash or a serialised string with a TTL that slides on each request.

await redis.set(`session:${sid}`, JSON.stringify(data), { EX: 3600 });
await redis.expire(`session:${sid}`, 3600); // sliding window

Rate limiting is a counter with an expiry. A fixed window is two commands; a sliding window uses a sorted set of timestamps.

const minute = Math.floor(Date.now() / 60_000);
const key = `rate:${ip}:${minute}`;
const replies = await redis.multi().incr(key).expire(key, 60).exec();
if (replies[0] > 100) throw new Error("rate_limited");

Leaderboards are a sorted set. ZADD records a score, ZINCRBY adjusts it, and ZREVRANGE returns the top players with scores. ZRANK gives a single player’s position, which is exactly what a profile page needs.

ZADD leaderboard 1500 ada
ZINCRBY leaderboard 50 ada
ZREVRANGE leaderboard 0 9 WITHSCORES
ZRANK leaderboard ada

All three patterns rely on the same two properties: commands are atomic, and every key can expire.

Queues and pub/sub

Lists make a dependable work queue. Producers LPUSH, workers BRPOP — the blocking pop means a worker sleeps until a job arrives instead of polling.

LPUSH queue:emails "welcome:42"
BRPOP queue:emails 30

For a reliable queue, move jobs to a processing list before handling them, and remove them only on success. That way a crashed worker does not silently lose a job.

Pub/sub is fire-and-forget messaging: publishers PUBLISH, subscribers receive on channels. It is excellent for live notifications and cache-busting signals, but there is no persistence and no delivery guarantee. If a subscriber is offline, the message is gone.

PUBLISH notifications:user:42 "You have a new follower"
SUBSCRIBE notifications:user:42

When you need persistence, acknowledgement and replay, use streams with consumer groups instead. Streams are the modern answer to “I want pub/sub, but reliable”.

Pipelines, transactions and Lua

A command is a network round trip. If you need ten values, ten sequential GETs spend most of their time waiting on the network. A pipeline sends them together and reads all replies at once.

const [name, plan, logins] = await redis
  .multi()
  .hget("user:42", "name")
  .hget("user:42", "plan")
  .hget("user:42", "logins")
  .exec();

A pipeline is not automatically a transaction. Wrapping commands in MULTI/EXEC makes them run as one atomic block, with no other client’s commands interleaved. Redis does not roll back on a command error the way SQL does; it simply continues, so validate inputs before you queue them.

For logic that must run atomically on the server, use a Lua script. It executes as a single command, can read and write several keys, and is the correct tool for compare-and-set operations such as releasing a lock only if you still own it.

const release = `
  if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
  end
  return 0
`;

await redis.eval(release, { keys: ["lock:order:1001"], arguments: [token] });

Scripts should be small and deterministic. Do not run anything unbounded inside one, because the whole server waits for it.

Persistence: RDB and AOF

In-memory does not have to mean ephemeral, but durability is a trade-off you choose.

RDB writes point-in-time snapshots of the dataset to disk, either on a schedule or on demand. Snapshots are compact, restore quickly, and are ideal for backups. The cost is that a crash loses everything written since the last snapshot.

SAVE     # blocking snapshot, avoid in production
BGSAVE   # fork and snapshot in the background

AOF appends every write command to a log. With appendfsync everysec you lose at most about a second of writes; with always you lose almost nothing but pay a much higher write cost. AOF files can be rewritten in the background to stay compact.

CONFIG SET appendonly yes
CONFIG SET appendfsync everysec

Many deployments enable both: RDB for fast restores and AOF for a small loss window. If you use Redis purely as a cache, you can turn persistence off entirely and let a cold cache refill from the database. If you use it for queues or counters you care about, keep persistence on and understand exactly how much data a crash can cost.

Replication, Sentinel and Cluster

A replica connects to a primary and receives a stream of writes, so it holds a near-real-time copy. Replicas can serve reads, which offloads the primary, and they are the basis for failover.

Sentinel watches a primary and its replicas and promotes a replica when the primary fails. It also tells clients the new primary’s address. Sentinel gives high availability for a dataset that fits on one node.

Redis Cluster shards data across nodes using 16,384 hash slots. Each key maps to a slot by hashing its name, and each node owns a range of slots. Clients can talk to any node and are redirected to the right one. Cluster gives horizontal scale and failover, at the cost of complexity: multi-key operations must keep keys in the same slot, usually through hash tags such as user:{42}:name.

The usual path is a single instance, then a replica, then Sentinel, then Cluster only when memory no longer fits on one machine.

Common anti-patterns

  • Redis as the only database. Without persistence and replication tuned for durability, a crash can lose data. Keep a durable system of record.
  • Unbounded keys. Every cached key needs a TTL, or memory fills and eviction starts dropping things you needed.
  • Big keys and big collections. A single hash with millions of fields, or a list with millions of entries, is slow to delete and can block the server.
  • KEYS in production. It blocks the event loop. Use SCAN.
  • One key for everything. Serialising a whole object on every change causes write amplification. Use hashes and update fields.
  • Ignoring the eviction policy. allkeys-lru can evict sessions and locks, not just cache entries.

Connecting from Node

Two clients dominate the Node ecosystem: node-redis and ioredis. Both speak the same protocol and expose one method per command, so the choice usually comes down to API taste and clustering support.

import { createClient } from "redis";

const redis = createClient({ url: process.env.REDIS_URL });
redis.on("error", (err) => console.error("redis", err));
await redis.connect();

await redis.set("health", "ok", { EX: 60 });
const value = await redis.get("health");

Create the client once at startup and share it. A connection is a socket with a command queue, and opening one per request adds latency and file descriptors. Always attach an error handler: without one, a dropped connection can surface as an unhandled exception.

Most commands return plain values, but options are passed as an object. SET accepts { EX: 300 } for seconds, { NX: true } to write only if the key is absent, and { KEEPTTL: true } to preserve an existing expiry. NX plus a TTL is the standard recipe for a distributed lock.

const acquired = await redis.set(`lock:order:${id}`, token, {
  NX: true,
  EX: 30,
});

For a sharded deployment, choose a client that understands cluster redirections and hash tags. Both major clients do; the important part is to keep related keys in the same slot when a command touches more than one.

Monitoring a running instance

A few commands tell you almost everything about a Redis instance’s health.

INFO memory
INFO stats
DBSIZE
SLOWLOG GET 10

INFO memory reports used_memory, maxmemory and the current policy. INFO stats includes keyspace_hits and keyspace_misses, whose ratio is your cache hit rate — the number to watch when tuning TTLs. DBSIZE counts keys, and SLOWLOG records commands that exceeded a latency threshold.

CONFIG SET slowlog-log-slower-than 10000  # microseconds
CONFIG RESETSTAT

Two more tools are useful but dangerous. MONITOR streams every command the server processes in real time; it is invaluable for debugging and too expensive to leave on in production. SCAN walks the keyspace in cursor-sized batches and is safe, unlike KEYS.

Watch three numbers on a dashboard: memory usage against maxmemory, the eviction count, and the hit rate. A falling hit rate with rising evictions usually means TTLs are too short or the dataset no longer fits, and it is better to add memory or fix the keys than to raise the limit and let the instance swap.

Best practices

  • Set a TTL on every cached key and pick an eviction policy that matches the data.
  • Use a consistent object:id:attribute naming scheme and document it.
  • Choose the data structure that fits the access pattern before writing code.
  • Reuse one client connection and pipeline independent commands.
  • Use Lua or MULTI for atomic multi-step operations, and keep them small.
  • Prefer SCAN over KEYS, and cap the size of any single key.
  • Enable persistence and replication for data you cannot rebuild, and test a restore.
  • Monitor memory, hit rate, evictions and slow commands before they become incidents.

Common mistakes

  • Caching without a TTL and slowly exhausting memory.
  • Using KEYS * to find keys and blocking every other client.
  • Treating pub/sub as a reliable queue and losing messages when a subscriber is down.
  • Storing a large object as one JSON string and rewriting it for every small change.
  • Assuming MULTI rolls back like a SQL transaction; it does not.
  • Letting one hot key or one huge sorted set become a bottleneck.
  • Forgetting that allkeys-lru can evict sessions, locks and rate-limit counters.
  • Running Redis with persistence off and no replica, then losing data on restart.

Where to go next

Redis is the practical face of caching, and understanding it makes the caching patterns on the backend roadmap concrete. Pair it with a durable system of record: PostgreSQL for relational data or MongoDB for documents. Then connect it to a Node service and revisit the Node.js basics to see how the client fits into your request path.

In practice

Four patterns you will use constantly

A cache read, a hash-backed object, a leaderboard and a rate limiter cover a large share of real Redis usage.

cache.js
import { createClient } from "redis";

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

async function getUser(id) {
  const key = `user:${id}`;
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const user = await db.users.findById(id);
  if (user) {
    await redis.set(key, JSON.stringify(user), { EX: 300 });
  }
  return user;
}

Caching with an expiry

A TTL bounds staleness and reclaims memory on its own. A key without an expiry lives until someone remembers to delete it.

Prefer
await redis.set(
  key,
  JSON.stringify(user),
  { EX: 300 },
);
Avoid
// No expiry: stale data lives
// until the key is deleted.
await redis.set(key, JSON.stringify(user));

Pipelining round trips

Each command is a network round trip. Group independent commands in a pipeline so they travel together.

Prefer
const [a, b, c] = await redis
  .multi()
  .get("a")
  .get("b")
  .get("c")
  .exec();
Avoid
const a = await redis.get("a");
const b = await redis.get("b");
const c = await redis.get("c");
// three sequential round trips

Trade-offs

When Redis earns its place

Redis is an outstanding cache and coordination layer, but it is not a replacement for a durable primary database.

Strengths

  • Speed that changes designs

    Microsecond reads make sessions, rate limiting and leaderboards practical inside the request path.

  • One tool, many structures

    Counters, queues, sets and rankings are built in, so you do not stand up a separate service for each one.

  • Simple to operate

    A single process, a small command set and mature clients mean Redis is easy to run and reason about.

Trade-offs

  • Memory is the limit

    The whole dataset lives in RAM, which costs far more per gigabyte than disk. The eviction policy decides what gets dropped.

  • Durability is a choice

    RDB snapshots and AOF reduce data loss but never eliminate it. A crash can still lose the most recent writes.

  • Single-threaded commands

    One slow command, such as KEYS or a huge ZRANGE, blocks every other client. Keep commands small and bounded.

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Redis?

Our interactive tutorial walks you through Redis step by step — with quizzes and real code you can run in the browser.