Home / Articles / Redis Beyond Caching: Sessions, Rate Limits, Queues, and Pub/Sub in Node.js

This article is published in English.

Redis Beyond Caching: Sessions, Rate Limits, Queues, and Pub/Sub in Node.js

Seven Redis patterns for Node.js backends — caching with TTLs, OTP keys, rate limits, BullMQ jobs, sessions, Pub/Sub limits, and when not to use Redis.

1424 words

Early mental models treat Redis as “just a cache”: store a value, set an expiry, read it faster than the database, done.

That picture is not wrong. It is incomplete.

In practice Redis shows up for hot API responses, shared sessions, abuse counters, one-time codes, deferred job queues, lightweight live fan-out, and other short-lived values.

A better framing: treat Redis as a fast in-memory data store with many backend roles — caching is only the first.

1. Redis can make an API much faster

Start with caching.

Consider GET /products.

Without a cache, each request may look like:

Client → Node.js API → PostgreSQL → Node.js API → Client

An expensive query under thousands of hits means the database repeats the same work.

Redis can sit in front of that query.

Client → Node.js API → Redis

On a hit, return immediately. On a miss, query PostgreSQL, store the result in Redis, then return it.

A simple ioredis sketch:

import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
async function getProducts() {
  const cached = await redis.get("products");
  if (cached) {
    return JSON.parse(cached);
  }
  const products = await database.product.findMany();
  await redis.set(
    "products",
    JSON.stringify(products),
    "EX",
    300
  );
  return products;
}h

Here EX means the cached payload expires after 300 seconds.

There is still a hard problem: cache invalidation.

Suppose Redis holds product:123 → price:500 while PostgreSQL now stores 600. The database is correct; Redis may still serve 500.

Caching is not “put everything in Redis.” Teams still need to reason about:

  • TTLs
  • Invalidation
  • Stale data
  • Misses
  • Cache failures

Putting a value in Redis is easy. Keeping it correct is harder.

2. Redis fits temporary data well

Short-lived values are a natural fit: one-time login codes, reset links, verification tokens, ephemeral session blobs, abuse counters, and advisory locks.

Example: issue a one-time code:

const otp = "482913";
await redis.set(
  `otp:${userId}`,
  otp,
  "EX",
  300
);

The OTP expires after five minutes automatically.

No dedicated OTP table is required. No separate cleanup job is required either.

Read it back with:

const otp = await redis.get(`otp:${userId}`);

After the TTL, Redis removes the key according to its expiration mechanism.

That pattern makes Redis a natural home for short-lived application data.

3. Redis can help with rate limiting

Take POST /login.

Without limits, a client can flood login attempts — thousands of requests in a row.

Redis can hold a counter for the client:

const key = `login-attempts:${ip}`;
const attempts = await redis.incr(key);
if (attempts === 1) {
  await redis.expire(key, 60);
}
if (attempts > 10) {
  throw new Error("Too many requests");
}

The shape is: IP → Redis counter → count → limit.

This matters more with multiple API servers behind a load balancer. Per-process in-memory counters are not global. Redis gives those servers a shared store.

4. Redis can power background jobs

Account creation may need to create the user, send a welcome email, generate data, notify another service, and run other work.

The HTTP request should not wait for all of that.

Push work onto a queue instead:

Client → API → Queue → Response

Then:

Queue → Worker → Process job

BullMQ is a common Node.js option that uses Redis:

await emailQueue.add("welcome-email", {
  userId: user.id,
  email: user.email,
});

A worker processes separately:

const worker = new Worker(
  "email",
  async (job) => {
    if (job.name === "welcome-email") {
      await sendWelcomeEmail(job.data.email);
    }
  },
  {
    connection: redisConnection,
  }
);

The API need not wait on the email provider before answering the user.

This fits work that is slow, retryable, dependent on external services, CPU-heavy, or not required before the API responds.

Split responsibilities: the API handles the request; the worker handles the heavy work.

5. Redis can store sessions

Session management is another fit. A key such as session:abc123 might hold:

{
  "userId": "123",
  "role": "ADMIN"
}

That becomes valuable with several backend instances sharing one session store through Redis.

Important distinction: Redis does not automatically make authentication secure.

Teams still must handle session identifiers, secure cookies, expiration, CSRF where applicable, authentication, and authorization.

Redis is infrastructure, not a security strategy.

6. Redis can help with real-time features

Pub/Sub can fan events across instances. One path looks like a client hitting server A, a publish into Redis, a subscription handler on server B, then delivery to another client.

Illustration:

await redis.publish(
  "notifications",
  JSON.stringify({
    userId: "123",
    message: "Your order has shipped",
  })
);
await subscriber.subscribe("notifications")
subscriber.on("message", (channel, message) => {
  console.log(channel, message);
});

Useful for notifications, live updates, chat-related flows, and event propagation.

Limitation: Redis Pub/Sub is not a durable message queue.

If durable processing, retries, or guaranteed delivery matter, prefer a queue or Redis Streams depending on the case.

Knowing that difference matters.

7. Redis becomes a problem if it is used everywhere

The most important lesson: once Redis is available, it is tempting to put everything there.

Do not.

Speed alone does not mean every datum belongs in memory.

PostgreSQL can remain the permanent source of truth for business data, while Redis handles cache, sessions, OTPs, rate limits, and queues.

A practical split keeps durable business records in PostgreSQL and reserves Redis for hot paths, short TTLs, and supporting workloads such as queues or counters.

Drawing that line early avoids many later redesigns.

Redis data structures matter

Redis is not only key → string. It offers several structures.

Strings

Simple values, such as user:123:name → "Mit".

Hashes

Multiple fields under one key:

user:123
name → Mit
role → ADMIN
email → example@email.com

Lists

Ordered collections and some queue-like patterns.

Sets

Unique values.

Sorted sets

Elements ordered by score — for example a leaderboard:

1000 → Player A
900  → Player B
800  → Player C

Choosing the right structure often simplifies the problem.

Common Redis mistakes to avoid

Mistake 1: caching everything

Not every query needs a cache. Caching adds complexity. If a query is already fast enough, Redis may solve a problem that does not exist.

Mistake 2: no expiration

Temporary data without TTLs accumulates. If data need not live forever, give it an expiration policy.

Mistake 3: treating Redis as the permanent database

If Redis holds the only copy of critical business data, the system has a serious dependency. Know the source of truth.

Mistake 4: ignoring Redis failures

Decide what happens when Redis is down. For many cache uses, falling back to the database is acceptable. The right strategy depends on the role Redis plays.

Mistake 5: using Redis without understanding the workload

Fast is not infinite. Still reason about memory, eviction, connections, key design, TTLs, serialization, network latency, and persistence needs.

How Redis changes backend thinking

Many designs begin as a request handler talking straight to SQL.

Once an in-memory store and deferred workers appear, the path often grows: the handler checks Redis first, then the database; or it enqueues work for a worker that talks to an external provider.

Backends become collections of specialized pieces. Redis is one piece that can appear in more than one role.

Closing note

Do not adopt Redis because everyone else did.

Reach for it when the fit is clear: caching for hot reads, TTL keys for short-lived secrets, counters for abuse limits, queues for deferred work, a shared session store across instances, or Pub/Sub when lightweight fan-out is enough.

Avoid making Redis the default answer to every backend question.

Good systems are not the ones with the longest tool lists. They are the ones where each tool earns its place.