Skip to content
Node.js nd database 4 min read

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.

ClientPackageStrengths
ioredisioredisCluster/Sentinel, robust reconnection, Lua helpers
node-redisredisOfficial, 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 a redis:// or TLS rediss:// URL. CommonJS users write const 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:field to 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 error handler and configure TLS (rediss://) plus a password for any non-local instance.
Last updated June 14, 2026
Was this helpful?