Skip to main content
Back to Blog
Tutorials
2 min read
November 7, 2024

How to Set Up Redis Caching in Next.js

Add Redis caching to your Next.js app for blazing-fast data access. Upstash Redis, cache patterns, and invalidation strategies.

Ryel Banfield

Founder & Lead Developer

Redis caching dramatically reduces database load and API response times. Upstash provides serverless Redis that works perfectly with Next.js on Vercel.

Step 1: Install Upstash Redis

pnpm add @upstash/redis

Step 2: Configure Redis Client

// lib/redis.ts
import { Redis } from "@upstash/redis";

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

Step 3: Basic Cache Pattern

// lib/cache.ts
import { redis } from "./redis";

export async function cached<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttl: number = 3600 // seconds
): Promise<T> {
  // Try cache first
  const cached = await redis.get<T>(key);
  if (cached !== null) {
    return cached;
  }

  // Fetch fresh data
  const data = await fetcher();

  // Store in cache
  await redis.set(key, data, { ex: ttl });

  return data;
}

Step 4: Cache Database Queries

// lib/data.ts
import { cached } from "@/lib/cache";
import { db } from "@/db";
import { posts } from "@/db/schema";
import { eq } from "drizzle-orm";

export async function getPopularPosts() {
  return cached(
    "posts:popular",
    async () => {
      return db
        .select()
        .from(posts)
        .orderBy(desc(posts.views))
        .limit(10);
    },
    600 // Cache for 10 minutes
  );
}

export async function getPostBySlug(slug: string) {
  return cached(
    `post:${slug}`,
    async () => {
      const [post] = await db
        .select()
        .from(posts)
        .where(eq(posts.slug, slug))
        .limit(1);
      return post || null;
    },
    1800 // Cache for 30 minutes
  );
}

Step 5: Cache Invalidation

// lib/cache.ts
import { redis } from "./redis";

export async function invalidate(key: string) {
  await redis.del(key);
}

export async function invalidatePattern(pattern: string) {
  const keys = await redis.keys(pattern);
  if (keys.length > 0) {
    await redis.del(...keys);
  }
}
// When updating a post
async function updatePost(slug: string, data: PostUpdate) {
  await db.update(posts).set(data).where(eq(posts.slug, slug));

  // Invalidate specific post cache and related lists
  await invalidate(`post:${slug}`);
  await invalidatePattern("posts:*");
}

Step 6: Cache API Responses

// app/api/products/route.ts
import { NextResponse } from "next/server";
import { cached } from "@/lib/cache";

export async function GET() {
  const products = await cached(
    "api:products",
    async () => {
      const res = await fetch("https://api.example.com/products");
      return res.json();
    },
    300 // Cache for 5 minutes
  );

  return NextResponse.json(products);
}

Step 7: Stale-While-Revalidate Pattern

export async function swr<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttl: number = 3600,
  staleTtl: number = 7200
): Promise<T> {
  const cached = await redis.get<{ data: T; timestamp: number }>(key);

  if (cached) {
    const age = (Date.now() - cached.timestamp) / 1000;

    if (age < ttl) {
      // Fresh - return cached
      return cached.data;
    }

    if (age < staleTtl) {
      // Stale - return cached but revalidate in background
      fetcher().then(async (data) => {
        await redis.set(
          key,
          { data, timestamp: Date.now() },
          { ex: staleTtl }
        );
      });
      return cached.data;
    }
  }

  // Miss or expired - fetch fresh
  const data = await fetcher();
  await redis.set(
    key,
    { data, timestamp: Date.now() },
    { ex: staleTtl }
  );
  return data;
}

Step 8: Session Storage with Redis

// lib/session.ts
import { redis } from "./redis";
import { cookies } from "next/headers";

interface Session {
  userId: string;
  email: string;
  role: string;
}

export async function getSession(): Promise<Session | null> {
  const cookieStore = await cookies();
  const sessionId = cookieStore.get("session_id")?.value;
  if (!sessionId) return null;

  return redis.get<Session>(`session:${sessionId}`);
}

export async function setSession(sessionId: string, data: Session) {
  await redis.set(`session:${sessionId}`, data, {
    ex: 60 * 60 * 24 * 7, // 7 days
  });
}

export async function destroySession(sessionId: string) {
  await redis.del(`session:${sessionId}`);
}

Step 9: Rate Limiter with Redis

export async function checkRateLimit(
  key: string,
  limit: number,
  window: number
): Promise<{ allowed: boolean; remaining: number }> {
  const current = await redis.incr(key);

  if (current === 1) {
    await redis.expire(key, window);
  }

  return {
    allowed: current <= limit,
    remaining: Math.max(0, limit - current),
  };
}

// Usage
const { allowed, remaining } = await checkRateLimit(
  `ratelimit:${ip}`,
  10, // 10 requests
  60  // per 60 seconds
);

Step 10: Cache Monitoring

// app/api/admin/cache/route.ts
import { redis } from "@/lib/redis";

export async function GET() {
  const info = await redis.info();
  const dbSize = await redis.dbsize();

  return NextResponse.json({
    connectedClients: info.connected_clients,
    usedMemory: info.used_memory_human,
    totalKeys: dbSize,
  });
}

Need High-Performance Architecture?

We build web applications with Redis caching, optimized databases, and scalable infrastructure. Contact us to discuss your project.

RediscachingperformanceNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles