Exploring the NextJS Cache API

February 13, 2025

Introduction

Caching is one of the most critical aspects of web performance, ensuring fast response times while reducing unnecessary backend load. Next.js, with its flexible data-fetching strategies, offers a robust caching system that can be fine-tuned for different use cases.

Recently, I experimented with Next.js’s caching mechanisms, particularly the cache() API, and explored how it interacts with data-fetching strategies like ISR, SSR, and streaming in server components.

In this post, I’ll share a deep dive into how Next.js handles caching, how to optimize its behavior, and some of the pitfalls I encountered along the way.

Understanding Next.js Caching

Next.js has a layered caching approach that determines whether a request should be served from cache or trigger new data fetching. These layers include:

1. Build-Time Caching (SSG & ISR)

When using Static Site Generation (SSG), Next.js pre-renders pages at build time and caches them indefinitely unless revalidated.

• Pages built with getStaticProps in older Next.js versions behave this way.

• In the App Router (app/ directory), static pages are cached automatically unless fetch() is explicitly configured otherwise.

Incremental Static Regeneration (ISR) allows these static pages to update dynamically while still leveraging cache:

export async function GET() { const res = await fetch('https://api.example.com/data', { next: { revalidate: 60 }, // Revalidate cache every 60 seconds }); return Response.json(await res.json()); }

Here, Next.js will serve cached data until 60 seconds have passed, after which it fetches fresh data.

2. Per-Request Caching (SSR & Server Components)

For Server-Side Rendering (SSR), pages are generated on every request unless explicitly cached.

With the App Router, caching can be controlled at the fetch level:

export async function GET() { const res = await fetch('https://api.example.com/data', { cache: 'no-store', // Disables caching }); return Response.json(await res.json()); }

This guarantees fresh data on every request but increases load on the backend.

3. Function-Level Caching (cache())

The cache() API in Next.js provides an additional layer of control, allowing you to cache function results across multiple calls within the same request lifecycle:

import { cache } from 'next/cache'; const getData = cache(async (id: string) => { const res = await fetch(`https://api.example.com/data/${id}`); return res.json(); }); export async function GET(req: Request) { const data = await getData('123'); // This will be cached for this request return Response.json(data); }

By using cache(), we can avoid multiple redundant API calls for the same request.

Customizing Cache Behavior

1. Setting Up Fine-Grained Cache Control

Next.js fetch requests support multiple caching strategies. Here’s how they differ:

Cache ModeDescriptionUse Case
force-cacheUses cached data even if staleStatic pages
no-storeAlways fetches fresh dataLive updates, authenticated requests
revalidate: XStale-while-revalidate mechanismNews feeds, dashboards
cache: 'default'Uses Next.js defaults based on the rendering methodGeneral use

Example: Using force-cache to store expensive API requests in cache:

const response = await fetch('https://api.example.com/stats', { cache: 'force-cache', });

This ensures that repeated API calls within a short period won’t hit the backend.

2. Revalidating Data Dynamically

While revalidate: X automatically refreshes data at set intervals, sometimes you need manual cache invalidation. Next.js provides a way to do this via On-Demand Revalidation:

import { revalidatePath } from 'next/cache'; export async function POST(req: Request) { const { path } = await req.json(); revalidatePath(path); // Clears cache for a specific path return Response.json({ success: true }); }

This is useful for CMS integrations where content updates need to be reflected instantly.

Caching in Server Components

Server Components in Next.js naturally cache fetch requests unless cache: 'no-store' is specified. However, when streaming large amounts of data, cache handling becomes crucial to avoid delays.

Example: Fetching and streaming a large dataset efficiently with caching enabled:

export default async function Page() { const data = await fetch('https://api.example.com/streamed-data', { next: { revalidate: 300 }, }).then((res) => res.json()); return ( <div> {data.map((item) => ( <p key={item.id}>{item.name}</p> ))} </div> ); }

Since this is a Server Component, the data will be cached and only refreshed every 5 minutes.

Challenges and Pitfalls

1. Conflicts Between SSR & ISR

When mixing no-store fetch requests inside an ISR page, caching can become inconsistent. If a page is statically generated but makes an SSR request inside a Server Component, it can cause hydration mismatches.

  • Fix: Use revalidate: X instead of no-store for better consistency.

2. Unexpected Cache Stale Issues

When caching API calls, certain headers (like authentication tokens) can prevent cache reuse.

  • Fix: Always set cache: 'no-store' for private data.
const response = await fetch('https://api.example.com/user', { headers: { Authorization: `Bearer ${token}` }, cache: 'no-store', });

3. Over-Caching Can Lead to Outdated Data

If you overuse force-cache or long revalidation periods, users might see outdated information.

  • Fix: Use revalidatePath() in API routes to refresh data dynamically.

Final Thoughts

After experimenting with Next.js’s cache API, I’ve found that:

  • ✅ Using cache() can optimize repeated function calls within the same request.
  • ✅ Revalidate strategies (revalidate: X) provide a balance between performance and fresh data.
  • ✅ Manual cache invalidation (revalidatePath) is crucial for real-time updates.
  • ✅ Avoid mixing SSR and ISR fetch behaviors to prevent caching conflicts.

Caching in Next.js is incredibly powerful, but it requires careful tuning depending on your use case.

Whether you’re building a statically optimized site or a real-time dashboard, understanding Next.js caching deeply can help you make better architectural decisions.

GitHub
X