Redis Use Cases in Real Applications with TypeScript Examples (and Where Valkey Fits)

By Pugazhenthi

Generic badgeGeneric badgeGeneric badge

Most developers first meet Redis as a cache. That description is correct, but incomplete.

Redis is a fast, in-memory data store with useful data structures — strings, hashes, lists, sets, sorted sets, streams, geospatial indexes and more. Those structures make it useful for caching, sessions, rate limiting, real-time messaging, queues, counters, leaderboards and other low-latency workloads.

This post walks the most common Redis use cases with practical TypeScript examples, then introduces Valkey and helps you choose between the two.

The examples use the official redis client for Node.js. Because Valkey maintains broad compatibility with the Redis OSS 7.2 command set, the same client and examples generally work with Valkey for the commands shown here. Always test compatibility before switching a production system.

1. Setting up Redis with TypeScript

Install the client:

npm install redis

Create one shared connection:

// redis.ts
import { createClient } from "redis";

export const redis = createClient({
  url: process.env.REDIS_URL ?? "redis://localhost:6379",
});

redis.on("error", (error) => {
  console.error("Redis error:", error);
});

export async function connectRedis(): Promise<void> {
  if (!redis.isOpen) {
    await redis.connect();
  }
}

A production URL normally looks like this:

redis://username:password@hostname:6379

Use rediss:// when the server requires TLS. Do not open Redis or Valkey directly to the public internet; use authentication, network restrictions and TLS where appropriate.

2. Database query caching

Caching is the best-known Redis use case. Instead of querying PostgreSQL or another primary database for the same data repeatedly, the application first checks Redis.

This is called the cache-aside pattern:

  1. Check the cache.
  2. Return the cached value if present.
  3. Otherwise, query the database.
  4. Store the result with an expiry time.
  5. Return the result.
type Product = {
  id: string;
  name: string;
  price: number;
};

async function getProduct(id: string): Promise<Product | null> {
  const key = `product:${id}`;
  const cached = await redis.get(key);

  if (cached) {
    return JSON.parse(cached) as Product;
  }

  const product = await db.query.products.findFirst({
    where: (products, { eq }) => eq(products.id, id),
  });

  if (!product) return null;

  await redis.set(key, JSON.stringify(product), {
    EX: 300, // five minutes
  });

  return product;
}

When the product changes, invalidate its cache entry:

await db.update(products).set(input).where(eq(products.id, id));
await redis.del(`product:${id}`);

Caching works well for product pages, dashboards, configuration, API responses and expensive calculations.

Important: Redis should not automatically become the source of truth. Decide what happens when the cache is unavailable, choose sensible TTLs, and invalidate data when the primary record changes.

3. User sessions

In a multi-instance Node.js application, storing sessions in process memory causes a problem: another server instance cannot see them. A shared Redis-compatible store solves this.

import { randomUUID } from "node:crypto";

type Session = {
  userId: string;
  role: "admin" | "user";
};

async function createSession(session: Session): Promise<string> {
  const sessionId = randomUUID();

  await redis.set(`session:${sessionId}`, JSON.stringify(session), {
    EX: 60 * 60 * 24 * 7, // seven days
  });

  return sessionId;
}

async function getSession(sessionId: string): Promise<Session | null> {
  const value = await redis.get(`session:${sessionId}`);
  return value ? (JSON.parse(value) as Session) : null;
}

async function revokeSession(sessionId: string): Promise<void> {
  await redis.del(`session:${sessionId}`);
}

This makes logout and forced revocation simple. Store only the data required to identify and authorize the session, and keep the session ID in a secure, HttpOnly, Secure, appropriately configured SameSite cookie.

4. Rate limiting

Redis counters are useful for protecting login, OTP, public API and form endpoints.

The following fixed-window limiter allows 100 requests per minute:

async function checkRateLimit(userId: string): Promise<boolean> {
  const minute = Math.floor(Date.now() / 60_000);
  const key = `rate:${userId}:${minute}`;

  const count = await redis.incr(key);

  if (count === 1) {
    await redis.expire(key, 60);
  }

  return count <= 100;
}

INCR is atomic, so simultaneous requests do not overwrite one another. However, INCR and EXPIRE are two commands. A process failure between them may leave a key without an expiry. In production, make the operation atomic with a Lua script:

const result = await redis.eval(`
  local count = redis.call('INCR', KEYS[1])
  if count == 1 then
    redis.call('EXPIRE', KEYS[1], ARGV[1])
  end
  return count
`, {
  keys: [`rate:${userId}:${Math.floor(Date.now() / 60_000)}`],
  arguments: ["60"],
});

const allowed = Number(result) <= 100;

A fixed window is easy to understand but can allow bursts near the window boundary. Use a sliding-window or token-bucket algorithm when smoother enforcement matters.

5. Counters and analytics

Redis can efficiently track views, likes, downloads and other rapidly changing counters.

await redis.incr(`post:${postId}:views`);
await redis.incrBy(`video:${videoId}:watch-seconds`, watchedSeconds);

const views = Number((await redis.get(`post:${postId}:views`)) ?? 0);

For analytics that must be permanently accurate, periodically persist aggregated values to your primary database or send the underlying events to a durable pipeline. An in-memory counter should not silently become your only financial or auditing record.

6. Leaderboards and rankings

Redis sorted sets store unique members together with numeric scores. This is ideal for game scores, sales rankings, course points and “top creators” lists.

await redis.zIncrBy("leaderboard:weekly", 25, userId);

const topUsers = await redis.zRangeWithScores(
  "leaderboard:weekly",
  0,
  9,
  { REV: true }
);

const zeroBasedRank = await redis.zRevRank("leaderboard:weekly", userId);
const rank = zeroBasedRank === null ? null : zeroBasedRank + 1;

Sorted sets also work for priority ordering and time-based indexes, where the score represents priority or a timestamp.

7. Real-time notifications with Pub/Sub

Redis Pub/Sub lets one process publish a message while multiple connected subscribers receive it.

Subscriber:

const subscriber = redis.duplicate();
await subscriber.connect();

await subscriber.subscribe("order-status", (message) => {
  const event = JSON.parse(message) as {
    orderId: string;
    status: string;
  };

  console.log("Order updated:", event);
});

Publisher:

await redis.publish(
  "order-status",
  JSON.stringify({ orderId: "ORD-1001", status: "ready" })
);

Pub/Sub is suitable for live UI updates, cache invalidation signals and transient notifications.

But Pub/Sub is at-most-once: if a subscriber disconnects, it misses messages sent during that period. Use Streams or a dedicated durable message broker when events must be retained and processed reliably.

8. Background jobs with Streams

Redis Streams retain messages and support consumer groups, allowing workers to divide jobs and acknowledge completed work.

Producer:

await redis.xAdd("jobs:email", "*", {
  type: "welcome-email",
  userId: "user-123",
  email: "person@example.com",
});

Create the consumer group once:

try {
  await redis.xGroupCreate("jobs:email", "email-workers", "0", {
    MKSTREAM: true,
  });
} catch (error) {
  // Ignore only the BUSYGROUP error; surface every other error.
}

Worker:

const response = await redis.xReadGroup(
  "email-workers",
  "worker-1",
  [{ key: "jobs:email", id: ">" }],
  { COUNT: 10, BLOCK: 5_000 }
);

if (response) {
  for (const stream of response) {
    for (const message of stream.messages) {
      await sendEmail(message.message);
      await redis.xAck("jobs:email", "email-workers", message.id);
    }
  }
}

The production design still needs retries, failure handling, idempotency, pending-message recovery and stream trimming. A mature queue library can provide these abstractions if you do not want to build them yourself.

9. Idempotency and duplicate prevention

Payment gateways and webhook providers may deliver the same event more than once. SET with NX creates the key only if it does not exist, which is useful for claiming an event ID.

async function claimWebhook(eventId: string): Promise<boolean> {
  const result = await redis.set(`webhook:${eventId}`, "processing", {
    NX: true,
    EX: 60 * 60 * 24,
  });

  return result === "OK";
}
if (!(await claimWebhook(event.id))) {
  return; // duplicate delivery
}

await processWebhook(event);

For payments and other critical workflows, also enforce uniqueness in the primary database. Redis can reject duplicates quickly, but it should not be the only integrity boundary.

10. Short-lived distributed locks

The same SET NX pattern can provide a basic lock so that only one application instance runs a task at a time.

import { randomUUID } from "node:crypto";

const lockKey = "lock:daily-report";
const lockToken = randomUUID();

const acquired = await redis.set(lockKey, lockToken, {
  NX: true,
  PX: 30_000,
});

if (acquired === "OK") {
  try {
    await generateDailyReport();
  } finally {
    await redis.eval(`
      if redis.call('GET', KEYS[1]) == ARGV[1] then
        return redis.call('DEL', KEYS[1])
      end
      return 0
    `, {
      keys: [lockKey],
      arguments: [lockToken],
    });
  }
}

The unique token prevents one worker from deleting a lock that expired and was later acquired by another worker.

This is appropriate for simple, short-lived coordination. Locks in systems where failure could cause financial loss or data corruption require deeper analysis of expiry, failover, clock assumptions, fencing tokens and the guarantees of the chosen locking design.

Redis geospatial commands can store locations and find nearby entries — useful for shops, delivery partners, service technicians and pickup points.

await redis.geoAdd("shops", [
  { longitude: 78.7047, latitude: 10.7905, member: "shop:trichy" },
  { longitude: 79.8428, latitude: 11.1271, member: "shop:thanjavur" },
]);

const nearby = await redis.geoSearch(
  "shops",
  { longitude: 78.7047, latitude: 10.7905 },
  { radius: 25, unit: "km" }
);

This is excellent for proximity lookup. Use a spatially capable database such as PostGIS when you need complex polygons, route-aware calculations or advanced geographic queries.

12. Introducing Valkey

Valkey is an open-source, high-performance key/value data store governed by the Linux Foundation and released under the BSD 3-Clause license. It began as a fork of Redis OSS 7.2.4 and describes itself as a vendor-neutral continuation of Redis OSS.

Valkey supports the familiar Redis-style data structures and workloads, including caching, queues, streams, Pub/Sub and use as a primary database. Valkey started from Redis OSS 7.2.4 and continues to add features on its own release line, while maintaining broad wire and command compatibility with the Redis OSS 7.2 command set. That compatibility means many existing Redis applications can move to Valkey with limited code changes — especially applications using commands available in Redis OSS 7.2.

For the examples in this post, switching may be as simple as changing the connection URL:

# Redis
REDIS_URL=redis://redis-host:6379

# Valkey
REDIS_URL=redis://valkey-host:6379

The environment variable can remain named REDIS_URL; it is only an application configuration name. You may prefer DATASTORE_URL or VALKEY_URL in a new Valkey-first project.

Compatibility does not mean that every newer Redis 8 capability, module, operational tool or hosted-service feature has an identical Valkey equivalent — and equally, Valkey has begun shipping its own features (such as per-slot dictionaries and experimental RDMA support) that do not exist in Redis. Validate commands, modules, persistence files, client behavior and failover in a staging environment before migration.

13. Redis vs Valkey

AreaRedisValkey
Project modelDeveloped and commercially backed by Redis Ltd.Vendor-neutral project under the Linux Foundation
Current open-source licenseRedis Open Source 8 offers RSALv2, SSPLv1 or AGPLv3BSD 3-Clause
OriginThe original Redis project and ecosystemForked from Redis OSS 7.2.4 in 2024
CompatibilityNative Redis commands and Redis-specific newer capabilitiesBroad compatibility with the Redis OSS 7.2 command set; the two projects are diverging on newer releases
EcosystemVery mature ecosystem, documentation, Redis Cloud and commercial toolingRapidly growing ecosystem with support from multiple cloud and infrastructure vendors
Search and additional data typesRedis 8 integrates Search, JSON, time series, Bloom and other capabilities into Redis Open SourceCore data structures plus separately evolving modules, including Valkey Search and Bloom
Best fitTeams wanting Redis 8 features, Redis Cloud, Redis enterprise capabilities or first-party commercial supportTeams prioritizing permissive licensing, neutral governance, portability and a Redis OSS-compatible core

A licensing clarification

You may have seen older comparisons saying that “Redis is no longer open source.” That statement is now outdated or at least incomplete.

Redis changed its license in 2024, which triggered the creation of Valkey. In Redis Open Source 8, Redis added AGPLv3 as a third licensing option alongside RSALv2 and SSPLv1. Valkey continues under the permissive BSD 3-Clause license.

The practical difference is not merely whether source code is visible. BSD-3-Clause and AGPLv3 impose very different obligations, while RSALv2 and SSPLv1 have their own conditions. Organizations embedding, modifying or offering the software as a service should have the relevant license terms reviewed rather than relying on a comparison table.

14. Which one should you choose?

Choose Redis when:

  • You want Redis Cloud or Redis’s first-party commercial products and support.
  • You need newer Redis-specific capabilities or the integrated Redis 8 data structures.
  • Your organization already standardizes on Redis and its licensing fits your use.
  • Your managed platform offers Redis with the features and SLA you need.

Choose Valkey when:

  • A permissive BSD license is important.
  • You prefer vendor-neutral Linux Foundation governance.
  • Your workload mainly uses the Redis OSS 7.2-compatible command set.
  • Portability across Valkey-supporting cloud providers matters.
  • You are starting a conventional cache, session, rate-limit, Streams or Pub/Sub workload and do not depend on Redis-specific extensions.

For many ordinary application workloads, both are technically capable. The decision is increasingly about licensing, governance, managed-service availability, operational tooling and the advanced features you actually require — not about a dramatic difference in basic GET, SET, hashes, sets or streams.

15. Production checklist

Whichever one you choose:

  1. Keep it on a private network and configure authentication and ACLs.
  2. Use TLS across untrusted networks.
  3. Set memory limits and choose an eviction policy intentionally.
  4. Monitor memory, latency, connections, rejected requests, replication and persistence.
  5. Decide whether you need RDB snapshots, AOF persistence, both or neither.
  6. Design for cache failure; avoid turning an optional cache into an application-wide single point of failure.
  7. Use replication, Sentinel/cluster features or a managed service when the availability requirement justifies them.
  8. Apply TTLs to temporary keys and use a consistent naming convention.
  9. Avoid unbounded collections and large blocking operations.
  10. Load-test with realistic key sizes, concurrency and failure conditions.

Final thoughts

Redis is much more than a place to cache JSON. Its data structures let you implement low-latency application patterns with relatively little code. Valkey offers a closely related, permissively licensed and vendor-neutral alternative.

Do not choose solely by brand or benchmark headlines. Start with your workload:

  • Is the data disposable or authoritative?
  • Must messages survive consumer downtime?
  • Which commands and modules do you require?
  • What failure and durability guarantees are needed?
  • Which managed service, license and support model fit the business?

Once those questions are answered, the Redis-versus-Valkey decision becomes much clearer.

Official references