Using Redis with Node.js
Redis is an in-memory data store that Node.js applications reach for whenever they need speed: caching expensive query results, sharing session state, rate limiting, queues, and lightweight pub/sub messaging. Because everything lives in RAM, operations finish in microseconds, and Redis ships with rich data structures — strings, hashes, lists, sets, and sorted sets — that map cleanly onto everyday application problems. This page covers connecting from Node, the core commands, working with those data structures, pub/sub, and using Redis as a cache layer in front of a slower database.
Choosing a client
Two clients dominate the Node ecosystem. ioredis is feature-rich with first-class Cluster and Sentinel support, while node-redis (the official client, imported as redis) tracks the latest Redis commands closely. Both are promise-based and work with async/await.
| Client | Package | Strengths |
|---|---|---|
| ioredis | ioredis | Cluster/Sentinel, robust reconnection, Lua helpers |
| node-redis | redis | Official, full command coverage, RESP3 support |
The examples below use ioredis, but the command names are identical to the Redis CLI, so they translate directly to node-redis.
Connecting
Install the client from npm, then create one shared connection for the lifetime of your process — a single connection multiplexes thousands of commands, so there is no need for a pool.
npm install ioredis
import Redis from "ioredis";
const redis = new Redis({
host: "localhost",
port: 6379,
password: process.env.REDIS_PASSWORD,
});
redis.on("error", (err) => console.error("Redis error", err));
const pong = await redis.ping();
console.log(pong);
Output:
PONG
You can also pass a connection string:
new Redis(process.env.REDIS_URL)accepts aredis://or TLSrediss://URL. CommonJS users writeconst Redis = require("ioredis").
Basic commands
The simplest Redis type is a string keyed by name. set and get store and retrieve values, and del removes them. Keys can be given a time-to-live so they expire automatically — essential for caches and sessions.
await redis.set("user:42:name", "Ada Lovelace");
const name = await redis.get("user:42:name");
console.log(name);
// Set with an expiry of 60 seconds (EX option)
await redis.set("session:abc", "token123", "EX", 60);
const ttl = await redis.ttl("session:abc");
console.log(`expires in ${ttl}s`);
// Atomic counters
await redis.set("visits", 0);
await redis.incr("visits");
await redis.incrby("visits", 5);
console.log(await redis.get("visits"));
Output:
Ada Lovelace
expires in 60s
6
incr/incrby are atomic, so concurrent requests never lose updates — perfect for view counts and rate limiting. Use expire(key, seconds) to add a TTL to an existing key, and persist(key) to remove it.
Working with data structures
Beyond strings, Redis gives you purpose-built structures. Hashes store field/value maps, ideal for objects; lists are ordered and double-ended, suited to queues; sets hold unique members with fast membership tests.
// Hash — store a user object
await redis.hset("user:42", { name: "Ada", role: "admin", logins: 3 });
const user = await redis.hgetall("user:42");
console.log(user);
// List — push and pop like a queue
await redis.rpush("jobs", "email", "resize", "index");
const next = await redis.lpop("jobs");
console.log(`processing: ${next}, remaining: ${await redis.llen("jobs")}`);
// Set — unique tags with membership tests
await redis.sadd("post:9:tags", "redis", "node", "redis");
console.log(await redis.smembers("post:9:tags"));
console.log(await redis.sismember("post:9:tags", "node"));
Output:
{ name: 'Ada', role: 'admin', logins: '3' }
processing: email, remaining: 2
[ 'redis', 'node' ]
1
Note that hash values come back as strings — Redis does not track types, so cast numbers yourself. Sorted sets (zadd, zrange) add a score to each member, which makes them the go-to choice for leaderboards and time-ordered feeds.
Pub/sub messaging
Redis can broadcast messages to subscribers on named channels, decoupling producers from consumers. A connection in subscriber mode cannot run normal commands, so use a separate connection for subscribing — duplicate() clones the configuration.
const sub = redis.duplicate();
const pub = redis;
await sub.subscribe("notifications");
sub.on("message", (channel, message) => {
console.log(`[${channel}] ${message}`);
});
await pub.publish("notifications", JSON.stringify({ type: "signup", id: 42 }));
Output:
[notifications] {"type":"signup","id":42}
Pub/sub is fire-and-forget: messages are not stored, so a subscriber that is offline misses them. For durable work queues, use a list with BLPOP or a Redis Stream instead.
Redis as a cache layer
The most common use of Redis is the cache-aside pattern: check Redis first, and only hit the slower database on a miss, storing the result with a TTL so it refreshes periodically.
async function getProduct(id) {
const cacheKey = `product:${id}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const product = await db.query("SELECT * FROM products WHERE id = $1", [id]);
// Cache for 5 minutes; future reads skip the database
await redis.set(cacheKey, JSON.stringify(product), "EX", 300);
return product;
}
When the underlying data changes, invalidate the entry with redis.del(cacheKey) so the next read repopulates it. Always set a TTL even on data you invalidate manually — it bounds staleness if an invalidation is ever missed.
Serialize structured values with
JSON.stringify/JSON.parse. For large hot objects, a Redis hash lets you read or update individual fields without rewriting the whole blob.
Best Practices
- Create one long-lived client at startup and reuse it; a single connection handles concurrent commands fine.
- Always set a TTL on cache keys so memory cannot grow unbounded and stale data self-heals.
- Use a consistent key naming scheme like
entity:id:fieldto keep the keyspace navigable. - Subscribe on a dedicated connection (via
duplicate()); a subscriber cannot issue other commands. - Prefer atomic operations (
incr,setnx, Lua scripts) over read-modify-write to avoid race conditions. - Pipeline or use
multi()to batch many commands into a single round trip when latency matters. - Attach an
errorhandler and configure TLS (rediss://) plus a password for any non-local instance.