~/blog $ cat nextjs-caching.mdx

Exploring the NextJS Cache API

·4 min read

$ grep "^##" nextjs-caching.mdx6 sections
  1. Introduction
  2. └ Understanding Next.js Caching
  3. └ Customizing Cache Behavior
  4. └ Caching in Server Components
  5. └ Challenges and Pitfalls
  6. └ Final Thoughts

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.

available for work