# Cara Kerja Caching Next.js: Static, Dynamic, dan Revalidate

> Beginner guide to how Next.js static and dynamic caching works: read build output symbols, generateStaticParams, revalidate, then check headers with curl.

URL: https://www.matthewswong.com/en/blog/cara-kerja-caching-rendering-nextjs-static-dynamic/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2026-10-17
- Updated: 2026-10-17
- Category: Web Dev

> **Key takeaway:** How Next.js caching works: a route renders statically, meaning its HTML is made at build time and shared by every visitor, unless its code reads cookies, headers, searchParams or other per-request data. Read the circle, dot and f symbols in the next build output, list paths with generateStaticParams, refresh with revalidate, and prove it with curl on production mode.

You change what a page shows, deploy, and the page still shows the old data. Another day it is the opposite: a page you thought was static is rendered again on every visit, or the time printed on the page never changes. It all comes down to one question: is this route rendered at build time, or when a request arrives?

This guide builds four small routes in Next.js 15 (App Router) and checks each one with two tools you always have: the next build output and the response headers from curl. Every option is matched against the Next.js documentation listed in the sources. I read the header function and the build output symbols in the Next.js 15.5.7 code installed in this site's project, and on this site's blog post pages I did see the headers x-nextjs-cache: HIT and cache-control: s-maxage=31536000.

## Want the concept as a simulation?

This guide focuses on steps and errors. Our free lesson explains the flow from build to a fresh page with an animation, plus a quiz on what makes a route dynamic and what the fetch default is in Next.js 15:

Static vs Dynamic Rendering in Next.js: Caching Explained

Work through this guide first so you have four routes to inspect, then open the lesson to see how this site uses them.

Expected output

## What you need

Everything runs on your laptop, with no server and no extra service.

A Next.js 15 project with the App Router and TypeScript, for example one made by create-next-app.

Two terminals. One runs npm run build and then npm start, the other runs curl. Every caching test here uses the production server, not npm run dev.

curl in the terminal, because we inspect the HTML and the response headers, not what a browser shows.

No database or external API is needed. Every example uses its own time and numbers so the result is predictable.

## Step 1: Create a static page and read the symbols in the build output

Start with a page that is clearly static. It prints the time at which it was rendered. If it really is rendered once at build time, that time will be frozen. Test it in production mode, because according to the docs, in development pages are always rendered on demand and never cached.

next build prints the route list with one symbol before each line, and /about is marked with the circle symbol (Static), meaning prerendered as static content. Two curl calls in a row return exactly the same time value: the build time, not the request time.

Only three symbols matter. The circle (Static): HTML made at build time. The filled dot (SSG): static HTML made from generateStaticParams. The f symbol (Dynamic): rendered on the server for every request. The legend is printed under the route list, so you never have to guess. A fourth symbol, the half circle (Partial Prerender), only appears when Partial Prerendering is on, which is outside this guide.

## Step 2: Create a dynamic route and see what flips it

Now a route that really must render per request. The most honest way is await connection() from next/server, which tells Next.js that rendering must wait for an incoming request. The docs use it for values such as new Date() or Math.random(). These have the same effect: cookies, headers, draftMode, the searchParams prop, unstable_noStore, and fetch with cache set to no-store. Read just one and the route is dynamic.

The build marks /now with the f symbol (Dynamic) while /about stays a circle. Every curl to /now returns a different time, because the HTML is made again on every request.

The biggest beginner trap: one line of cookies() or one searchParams prop in a static page turns the whole route from a circle into the f symbol, with no error message at all. Every time you add code that reads the request, run next build again and look at the symbol.

## Step 3: Prerender a parameterised route with generateStaticParams

A route such as /produk/[id] is one template with many paths. generateStaticParams returns an array of objects, one per path, and Next.js builds those pages at build time instead of on demand. In Next.js 15, params is a Promise, so you write await params. Then dynamicParams set to false makes a path that is not listed return 404. The default, true, renders that path the first time it is requested.

The build marks /produk/[id] with the filled dot (SSG), with some paths listed under it. After npm start, /produk/1 answers 200 and /produk/99 answers 404.

One more rule: generateStaticParams must always return an array, even an empty one. An empty array means no path is built at build time, but every path is rendered the first time it is visited and then stored. Returning nothing makes the route dynamic.

## Step 4: Add revalidate so a static page does not freeze

A static page can go stale. export const revalidate = 60 means: after 60 seconds the next request still gets the old page while a new version is built in the background, and the request after that gets the new one. The pattern is called stale-while-revalidate. The docs recommend a high value, such as an hour, and revalidatePath or revalidateTag when you need precision.

The first two curl calls show the same time. The first request after the 60 seconds still shows the old time, and the request after that shows a new one. If it still shows the old time, wait a moment and repeat: the new version is generated in the background.

The revalidate value must be statically analysable: 600 is valid, 60 * 10 is not. And the lowest revalidate across the layouts, pages and fetch calls of a route sets the frequency for the whole route.

## Step 5: Set fetch and route handlers on purpose

Two defaults changed in Next.js 15 and make many older tutorials misleading. fetch is not cached by default, and neither is a GET Route Handler. Opt in as needed: cache force-cache or next.revalidate for fetch, and dynamic force-static for the handler. fetch with no-store, or revalidate 0, makes the route dynamic.

With force-static added, /api/waktu shows as a circle in the build output and two curl calls return the same builtAt. Remove the line and the route becomes the f symbol.

Two notes. First, a route that contains fetch calls without options can still be prerendered and its HTML cached; if you must guarantee a dynamic route, use connection(). Second, all of this describes Next.js 15. Next.js 16 introduces the Cache Components model with use cache, so check which version a tutorial describes before copying its config. The comparison is in 

the Cache Components and use cache article

.

## Step 6: Prove it with the response headers

The build symbols tell you what Next.js planned. The headers tell you what actually went out. In Next.js 15.5.7, the getCacheControlHeader function produces an s-maxage equal to the revalidate value, a one-year s-maxage (31536000 seconds) when revalidate is not set, and private, no-cache, no-store, max-age=0, must-revalidate when revalidate is 0.

/about carries s-maxage=31536000, and /produk/1 carries s-maxage=60 at the start of its value. The x-nextjs-cache and x-nextjs-prerender headers are not in the official docs, but they appear on prerendered pages in Next.js 15.5.7 (on this site's blog post pages I saw HIT and 1). Treat both as debugging hints, not a contract.

Cache-Control decides whether a browser or a CDN asks the server at all, while the render cache decides whether the server renders again. They are separate layers. If you set Cache-Control yourself in next.config, Next.js 15 no longer overrides it with its own default, so the header you wrote is the one that goes out.

Add NEXT_PRIVATE_DEBUG_CACHE=1 to your .env file before npm start. According to the ISR docs, the server console then logs ISR cache hits and misses, so you can see which pages are generated during the build and which are updated as paths are accessed.

## Common errors and how to fix them

Almost every caching problem in Next.js falls into these eight symptoms. Match yours against the first column.

Symptom

Cause

Fix

A page you expected to be static is the f symbol in the build output

The route reads cookies, headers, searchParams, connection, or fetch with no-store.

Find the API it touches, move it to another route or keep it out of that route, then rebuild and check the symbol.

The data changed but the page still shows the old version

The page is prerendered with no revalidate, or the window has not passed. The first request after expiry still gets the old version by design.

Set revalidate, or call revalidatePath or revalidateTag after the data changes. Refresh twice before concluding anything.

You test revalidate in npm run dev and nothing happens

In development, pages are always rendered on demand and never cached.

Run npm run build and then npm start, and test there.

A time or random number on the page never changes

The value was computed at build time in a static route, and the HTML is reused.

Add await connection() to render per request, or use revalidate if a periodic refresh is enough.

A valid-looking path returns 404 on the production server

dynamicParams is false and the path is not in generateStaticParams.

Add the path to the list, or remove dynamicParams so a new path renders on its first request.

A page got slower after upgrading to Next.js 15

fetch is no longer cached by default, so every render calls the data source again.

Add cache force-cache or next revalidate to fetch calls whose data may be reused.

A GET route handler shows as the f symbol though its content never changes

In Next.js 15, GET handlers are not cached by default.

Add export const dynamic = force-static to the route.ts file.

revalidate = 60 * 10 does not work the way you meant

The revalidate value must be statically analysable, and the docs call an expression like that invalid.

Write a literal number: revalidate = 600.

## Checklist before you call caching done

You tested with npm run build and npm start, not npm run dev.

Every route in the build output has the symbol you expected: circle, dot or f.

Parameterised routes have generateStaticParams, and you chose dynamicParams on purpose.

Pages whose data changes have a literal revalidate number, or are triggered by revalidatePath and revalidateTag.

The fetch calls and GET Route Handlers that may be cached are opted in explicitly.

You read the Cache-Control header with curl -I instead of guessing.

## Go deeper

This guide teaches how to check. The free lesson explains why: a simulation from generateStaticParams to background regeneration, real code from this site (the blog layout, the locale layout with dynamicParams false, the open-source page with revalidate), and a three-question quiz.

Open the lesson: Static vs Dynamic Rendering in Next.js

Once your routes are static on purpose, continue with the other optimisations in 

the Next.js performance optimisation guide

.

## Sources

- Next.js 15 docs: Incremental Static Regeneration
- Next.js 15 docs: Route Segment Config
- Next.js 15 docs: Partial Prerendering, static and dynamic rendering
- Next.js 15 docs: Caching and Revalidating
- Next.js blog: Next.js 15, caching semantics

## Source links

- https://nextjs.org/docs/15/app/guides/incremental-static-regeneration
- https://nextjs.org/docs/15/app/api-reference/file-conventions/route-segment-config
- https://nextjs.org/docs/15/app/getting-started/partial-prerendering
- https://nextjs.org/docs/15/app/getting-started/caching-and-revalidating
- https://nextjs.org/blog/next-15

## FAQ

### How do I tell whether a Next.js route is static or dynamic?

Run next build and read the symbol in front of each route. The circle means static, the filled dot means static HTML made from generateStaticParams, and the f symbol means rendered on demand. Then confirm with curl -I against next start, because next dev never caches pages.

### Why does my Next.js page still show old data after I changed it?

A prerendered page keeps its build-time HTML until it is revalidated. Set a revalidate number on the route, or call revalidatePath or revalidateTag after the data changes. Note that with time-based revalidation the first request after expiry still receives the old page while a new one is built in the background.

### Is fetch cached by default in Next.js 15?

No. Next.js 15 does not cache fetch requests by default, and GET Route Handlers are not cached either. Opt in with cache set to force-cache or next revalidate on the fetch call, and dynamic set to force-static on a route handler.

### Why does revalidate not work when I run npm run dev?

In development, the docs say pages are always rendered on demand and never cached, so a revalidate window has nothing to expire. Run npm run build and npm start to test incremental static regeneration the way production behaves.

### Does using cookies() make the whole Next.js page dynamic?

In a normal Next.js 15 setup, yes. A component that uses cookies, headers, connection, draftMode, the searchParams prop, unstable_noStore or fetch with no-store depends on the request, so the route cannot be prerendered. Keep that code in the route that needs it, and check the symbol in the next build output.

---

- HTML version: https://www.matthewswong.com/en/blog/cara-kerja-caching-rendering-nextjs-static-dynamic/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/cara-kerja-caching-rendering-nextjs-static-dynamic/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
