A

Redis

redis cache key-value in-memory session pubsub realtime

Redis

Redis (Remote Dictionary Server) is an in-memory data structure store used as a database, cache, message broker, and queue. Created in 2009, Redis is known for extreme speed—capable of millions of operations per second. For JavaScript/TypeScript developers, Redis is essential for caching, session storage, real-time features, and rate limiting.

What is Redis?

Redis is an in-memory key-value store:

import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

// Set key-value
await redis.set('user:123', JSON.stringify({ name: 'Alice', age: 25 }));

// Get value
const data = await redis.get('user:123');
const user = JSON.parse(data);

// Set with expiration (TTL)
await redis.set('session:abc', 'user123', 'EX', 3600); // Expires in 1 hour

// Delete
await redis.del('user:123');

Key Features:

  • In-memory: Extremely fast (microsecond latency)
  • Data structures: Strings, lists, sets, hashes, sorted sets
  • Persistence: Optional disk persistence
  • Pub/Sub: Real-time messaging
  • Atomic operations: Race-condition free
  • Clustering: Horizontal scaling

Why Redis?

1. Speed

Redis operates in-memory:

Database Query (Postgres):  ~5-50ms
Redis Cache:                ~0.1-1ms  (50x faster!)

2. Rich Data Structures

Beyond simple key-value:

// String
await redis.set('counter', '0');
await redis.incr('counter'); // 1
await redis.decr('counter'); // 0

// List (ordered collection)
await redis.lpush('tasks', 'task1', 'task2', 'task3');
await redis.lrange('tasks', 0, -1); // ['task3', 'task2', 'task1']

// Set (unique values)
await redis.sadd('tags', 'javascript', 'typescript', 'redis');
await redis.smembers('tags'); // ['javascript', 'typescript', 'redis']

// Hash (object)
await redis.hset('user:123', 'name', 'Alice', 'age', '25');
await redis.hgetall('user:123'); // { name: 'Alice', age: '25' }

// Sorted Set (ranked values)
await redis.zadd('leaderboard', 100, 'alice', 75, 'bob', 90, 'charlie');
await redis.zrange('leaderboard', 0, -1, 'WITHSCORES');
// ['bob', '75', 'charlie', '90', 'alice', '100']

3. Pub/Sub (Real-Time Messaging)

// Subscriber
const subscriber = new Redis();

subscriber.subscribe('notifications', (err, count) => {
  console.log(`Subscribed to ${count} channel(s)`);
});

subscriber.on('message', (channel, message) => {
  console.log(`Received on ${channel}:`, message);
});

// Publisher
const publisher = new Redis();
await publisher.publish('notifications', 'New message!');

4. Atomic Operations

Race-condition free operations:

// Atomic increment (safe with multiple clients)
await redis.incr('page:views');

// Atomic decrement
await redis.decr('inventory:item123');

// Get and set atomically
const oldValue = await redis.getset('key', 'newValue');

// Set if not exists (lock)
const acquired = await redis.setnx('lock:resource', 'locked');
if (acquired) {
  // Got the lock
}

Using Redis with TypeScript

pnpm add ioredis
pnpm add -D @types/ioredis
import Redis from 'ioredis';

// Connect
const redis = new Redis({
  host: 'localhost',
  port: 6379,
  password: 'your-password',
  // Or use URL
  // url: process.env.REDIS_URL,
});

// Basic operations
await redis.set('key', 'value');
const value = await redis.get('key');

// With TypeScript types
interface User {
  id: string;
  name: string;
  email: string;
}

const user: User = {
  id: '123',
  name: 'Alice',
  email: '[email protected]',
};

// Store JSON
await redis.set(`user:${user.id}`, JSON.stringify(user));

// Retrieve JSON
const data = await redis.get(`user:${user.id}`);
const retrievedUser: User = JSON.parse(data!);

With node-redis

pnpm add redis
import { createClient } from 'redis';

const client = createClient({
  url: process.env.REDIS_URL,
});

await client.connect();

// Set/Get
await client.set('key', 'value');
const value = await client.get('key');

// JSON (requires RedisJSON module)
await client.json.set('user:123', '$', {
  name: 'Alice',
  age: 25,
});

const user = await client.json.get('user:123');

Common Use Cases

1. Caching

async function getUser(id: string): Promise<User> {
  const cacheKey = `user:${id}`;

  // Check cache first
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }

  // Cache miss - query database
  const user = await db.user.findUnique({ where: { id } });

  // Store in cache for 1 hour
  await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);

  return user;
}

// Invalidate cache on update
async function updateUser(id: string, data: Partial<User>) {
  const user = await db.user.update({ where: { id }, data });

  // Delete from cache
  await redis.del(`user:${id}`);

  return user;
}

2. Session Storage

import session from 'express-session';
import RedisStore from 'connect-redis';
import Redis from 'ioredis';

const redisClient = new Redis(process.env.REDIS_URL);

app.use(
  session({
    store: new RedisStore({ client: redisClient }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
      maxAge: 1000 * 60 * 60 * 24, // 24 hours
      secure: process.env.NODE_ENV === 'production',
    },
  })
);

3. Rate Limiting

async function checkRateLimit(userId: string): Promise<boolean> {
  const key = `rate_limit:${userId}`;
  const limit = 100; // 100 requests
  const window = 60; // per minute

  const current = await redis.incr(key);

  if (current === 1) {
    // First request - set expiration
    await redis.expire(key, window);
  }

  return current <= limit;
}

// Usage in API
app.get('/api/data', async (req, res) => {
  const allowed = await checkRateLimit(req.user.id);

  if (!allowed) {
    return res.status(429).json({ error: 'Rate limit exceeded' });
  }

  // Process request...
});

4. Job Queue (with BullMQ)

pnpm add bullmq
import { Queue, Worker } from 'bullmq';

// Create queue
const emailQueue = new Queue('emails', {
  connection: {
    host: 'localhost',
    port: 6379,
  },
});

// Add job
await emailQueue.add('send-welcome', {
  to: '[email protected]',
  subject: 'Welcome!',
  body: 'Welcome to our app!',
});

// Process jobs
const worker = new Worker(
  'emails',
  async (job) => {
    console.log('Processing job:', job.data);
    await sendEmail(job.data);
  },
  {
    connection: {
      host: 'localhost',
      port: 6379,
    },
  }
);

5. Leaderboard

// Add score
await redis.zadd('leaderboard', 1250, 'alice');
await redis.zadd('leaderboard', 980, 'bob');
await redis.zadd('leaderboard', 1100, 'charlie');

// Get top 10
const topPlayers = await redis.zrevrange('leaderboard', 0, 9, 'WITHSCORES');
// ['alice', '1250', 'charlie', '1100', 'bob', '980']

// Get user rank (0-indexed)
const rank = await redis.zrevrank('leaderboard', 'alice'); // 0 (1st place)

// Get user score
const score = await redis.zscore('leaderboard', 'alice'); // 1250

// Increment score
await redis.zincrby('leaderboard', 50, 'alice'); // 1300

6. Distributed Locking

async function acquireLock(resource: string, ttl: number = 10): Promise<boolean> {
  const lockKey = `lock:${resource}`;
  const acquired = await redis.set(lockKey, 'locked', 'NX', 'EX', ttl);
  return acquired === 'OK';
}

async function releaseLock(resource: string): Promise<void> {
  await redis.del(`lock:${resource}`);
}

// Usage
async function processResource(id: string) {
  const locked = await acquireLock(id);

  if (!locked) {
    throw new Error('Resource is locked');
  }

  try {
    // Do work
    await doWork(id);
  } finally {
    await releaseLock(id);
  }
}

Redis + Framework Integration

With Next.js

// lib/redis.ts
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

export default redis;
// app/api/data/route.ts
import redis from '@/lib/redis';
import { NextResponse } from 'next/server';

export async function GET() {
  const cached = await redis.get('data');

  if (cached) {
    return NextResponse.json(JSON.parse(cached));
  }

  const data = await fetchData();
  await redis.set('data', JSON.stringify(data), 'EX', 300); // 5 minutes

  return NextResponse.json(data);
}

With Node.js/Express

import express from 'express';
import Redis from 'ioredis';

const app = express();
const redis = new Redis(process.env.REDIS_URL);

app.get('/api/users/:id', async (req, res) => {
  const { id } = req.params;
  const cacheKey = `user:${id}`;

  // Check cache
  const cached = await redis.get(cacheKey);
  if (cached) {
    return res.json(JSON.parse(cached));
  }

  // Query database
  const user = await db.user.findUnique({ where: { id } });

  // Cache for 1 hour
  await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);

  res.json(user);
});

app.listen(3000);

Redis Cloud Providers

Upstash (Serverless Redis)

Perfect for serverless/edge:

import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL,
  token: process.env.UPSTASH_REDIS_REST_TOKEN,
});

// Works in edge runtimes (Cloudflare Workers, Vercel Edge)
await redis.set('key', 'value');

Pricing:

  • Free: 10,000 commands/day
  • Pay-as-you-go: $0.20 per 100K commands

Redis Cloud (RedisLabs)

Official cloud offering:

Pricing:

  • Free: 30 MB
  • Paid: $7+/month

AWS ElastiCache

Managed Redis on AWS:

Pricing:

  • Starts at ~$15/month (t4g.micro)

Performance Tips

1. Use Pipelining

// ❌ Bad - multiple round trips
for (let i = 0; i < 1000; i++) {
  await redis.set(`key:${i}`, `value${i}`);
}

// ✓ Good - single round trip
const pipeline = redis.pipeline();
for (let i = 0; i < 1000; i++) {
  pipeline.set(`key:${i}`, `value${i}`);
}
await pipeline.exec();

2. Set Expiration

// ✓ Good - prevent memory bloat
await redis.set('session:123', 'data', 'EX', 3600);

// ❌ Bad - stays in memory forever
await redis.set('session:123', 'data');

3. Use Appropriate Data Structures

// ✓ Good - use hash for objects
await redis.hset('user:123', 'name', 'Alice', 'age', '25');

// ❌ Bad - storing as JSON string
await redis.set('user:123', JSON.stringify({ name: 'Alice', age: 25 }));

Persistence Options

RDB (Snapshotting)

Point-in-time snapshots:

# redis.conf
save 900 1      # Save after 900 seconds if 1 key changed
save 300 10     # Save after 300 seconds if 10 keys changed
save 60 10000   # Save after 60 seconds if 10000 keys changed

AOF (Append-Only File)

Log every write operation:

# redis.conf
appendonly yes
appendfsync everysec  # Fsync every second

Hybrid (RDB + AOF)

Best of both worlds (Redis 4.0+).

Redis vs. Other Caches

Feature Redis Memcached In-Memory JS Object
Data Structures Rich Key-value only Any
Persistence Optional No No
Pub/Sub Yes No No
Clustering Yes Yes No
Atomic Ops Yes Limited No (race conditions)
Best For Everything Simple cache Single process

Key Takeaways

  • In-memory data store (extremely fast)
  • Rich data structures: strings, lists, sets, hashes, sorted sets
  • Perfect for caching, sessions, rate limiting, queues
  • Pub/Sub for real-time messaging
  • Use ioredis for TypeScript
  • Upstash for serverless/edge deployments
  • Set expiration to prevent memory bloat
  • Atomic operations prevent race conditions

Redis is essential for modern web applications. Use it as a cache in front of your primary database to reduce latency by 50-100x. It's also perfect for sessions, rate limiting, real-time features, and job queues. For serverless deployments, use Upstash's HTTP-based Redis.

Last updated: October 16, 2025