Next.js Cache

beginner

Next.js built-in caching with unstable_cache, revalidatePath, and revalidateTag.

cachingnextjsserverlessvercel
Tested on201619TS5.9
$ bunx sinew add caching/nextjs-cache
Interactive demo coming soon

1The Problem

Adding external caching infrastructure is often overkill:

  • Redis requires additional infrastructure and cost
  • Managing cache invalidation across deployments is complex
  • Cold starts on serverless need fast cache warming

2The Solution

Use Next.js built-in caching for simple, zero-config caching that works out of the box on Vercel and other platforms.

3Files

lib/cache.ts

lib/cache.tsTypeScript
import { unstable_cache } from "next/cache";
import { revalidatePath, revalidateTag } from "next/cache";

// Cache a function with tags for invalidation
export function cachedQuery<T>(
  fn: () => Promise<T>,
  keyParts: string[],
  options?: {
    tags?: string[];
    revalidate?: number | false;
  }
) {
  return unstable_cache(fn, keyParts, {
    tags: options?.tags,
    revalidate: options?.revalidate,
  });
}

// Revalidate a specific path
export function invalidatePath(path: string, type?: "page" | "layout") {
  revalidatePath(path, type);
}

// Revalidate all entries with a tag
export function invalidateTag(tag: string) {
  revalidateTag(tag);
}

lib/data/users.ts

lib/data/users.tsTypeScript
import { prisma } from "@/lib/db";
import { cachedQuery, invalidateTag } from "@/lib/cache";

export const getUser = (id: string) =>
  cachedQuery(() => prisma.user.findUnique({ where: { id } }), ["user", id], {
    tags: [`user:${id}`, "users"],
    revalidate: 3600,
  })();

export const getUsers = () =>
  cachedQuery(() => prisma.user.findMany({ orderBy: { createdAt: "desc" } }), ["users"], {
    tags: ["users"],
    revalidate: 60,
  })();

export async function updateUser(id: string, data: { name: string }) {
  const user = await prisma.user.update({ where: { id }, data });
  invalidateTag(`user:${id}`);
  invalidateTag("users");
  return user;
}

app/users/[id]/page.tsx

app/users/[id]/page.tsxTypeScript
import { getUser } from "@/lib/data/users";
import { notFound } from "next/navigation";

export default async function UserPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const user = await getUser(id);

  if (!user) {
    notFound();
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

app/api/users/[id]/route.ts

app/api/users/[id]/route.tsTypeScript
import { NextRequest, NextResponse } from "next/server";
import { updateUser } from "@/lib/data/users";

export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const data = await req.json();
  const user = await updateUser(id, data);
  return NextResponse.json(user);
}

4Configuration

Cache Revalidation Options

// Revalidate every 60 seconds
{
  revalidate: 60;
}

// Never revalidate (static)
{
  revalidate: false;
}

// Don't specify - uses page/route segment config
{
}
TypeScript

Route Segment Config

// app/users/page.tsx
export const revalidate = 60; // Revalidate every 60 seconds
export const dynamic = "force-static"; // Force static generation
TypeScript

5Usage

On-Demand Revalidation

// In a Server Action or Route Handler
import { revalidatePath, revalidateTag } from "next/cache";

// Revalidate a specific page
revalidatePath("/users");

// Revalidate all pages using a tag
revalidateTag("users");

// Revalidate a layout (and all nested pages)
revalidatePath("/dashboard", "layout");
TypeScript

Webhook Revalidation

// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from "next/server";
import { revalidateTag } from "next/cache";

export async function POST(req: NextRequest) {
  const secret = req.headers.get("x-revalidate-secret");
  if (secret !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ error: "Invalid secret" }, { status: 401 });
  }

  const { tag } = await req.json();
  revalidateTag(tag);

  return NextResponse.json({ revalidated: true });
}
TypeScript

6Troubleshooting

Cache not invalidating

  • Ensure you're calling revalidateTag or revalidatePath in a Server Action or Route Handler
  • Check that tags match exactly (they're case-sensitive)
  • Remember that unstable_cache caches are per-deployment

Stale data in development

  • Next.js caching behaves differently in dev mode
  • Use next build && next start to test production caching behavior

Related patterns