Redis Use Cases in Real Applications with TypeScript Examples (and Where Valkey Fits)
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
redisclient 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 redisCreate 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:6379Use 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:
- Check the cache.
- Return the cached value if present.
- Otherwise, query the database.
- Store the result with an expiry time.
- 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.
11. Geospatial search
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:6379The 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
| Area | Redis | Valkey |
|---|---|---|
| Project model | Developed and commercially backed by Redis Ltd. | Vendor-neutral project under the Linux Foundation |
| Current open-source license | Redis Open Source 8 offers RSALv2, SSPLv1 or AGPLv3 | BSD 3-Clause |
| Origin | The original Redis project and ecosystem | Forked from Redis OSS 7.2.4 in 2024 |
| Compatibility | Native Redis commands and Redis-specific newer capabilities | Broad compatibility with the Redis OSS 7.2 command set; the two projects are diverging on newer releases |
| Ecosystem | Very mature ecosystem, documentation, Redis Cloud and commercial tooling | Rapidly growing ecosystem with support from multiple cloud and infrastructure vendors |
| Search and additional data types | Redis 8 integrates Search, JSON, time series, Bloom and other capabilities into Redis Open Source | Core data structures plus separately evolving modules, including Valkey Search and Bloom |
| Best fit | Teams wanting Redis 8 features, Redis Cloud, Redis enterprise capabilities or first-party commercial support | Teams 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:
- Keep it on a private network and configure authentication and ACLs.
- Use TLS across untrusted networks.
- Set memory limits and choose an eviction policy intentionally.
- Monitor memory, latency, connections, rejected requests, replication and persistence.
- Decide whether you need RDB snapshots, AOF persistence, both or neither.
- Design for cache failure; avoid turning an optional cache into an application-wide single point of failure.
- Use replication, Sentinel/cluster features or a managed service when the availability requirement justifies them.
- Apply TTLs to temporary keys and use a consistent naming convention.
- Avoid unbounded collections and large blocking operations.
- 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.