What Next.js gives you out of the box
- Data cache – In Next.js 13+ every
fetch()call lands in a per-request memory cache. Adding arevalidateoption tells the runtime to refresh the data after a set interval, turning a hot API call into a cold one only when needed. - Full-route cache – The framework persists the rendered HTML and the data needed for an entire route. Returning visitors see an instant transition because the server skips re-rendering.
- ISR – Static pages regenerate in the background while the old version keeps serving traffic. This lets you keep a large site static without a full rebuild each time content changes.
- Server Component
cache()– React’scachehelper memoizes expensive calculations or database connections for the duration of a single request, avoiding duplicate work inside a page’s component tree.
These caches solve the “first-hit” problem but still live inside the Node process. To protect the server and the database, push caching outward.
External layers you can add
| Layer | What it stores | Typical tool | How it helps |
|---|---|---|---|
| CDN | Static assets, HTML, API JSON | Cloudflare, Akamai, AWS CloudFront | Moves content to edge locations, shortening the round-trip to the user |
| Reverse proxy | Whole page responses before they reach Node | Nginx, Varnish | Serves cached pages directly, reducing load on the Next.js instance |
| Application-level cache | Results of expensive DB queries or API calls | Redis, Memcached | Provides a fast key-value store that survives server restarts and can be shared across multiple app instances |
Each layer sits farther from the origin database, so a miss at one level cascades to the next, ultimately reaching the database only when absolutely necessary.
Keeping caches fresh
Invalidation trips most teams up. Three practical patterns work well:
- Time-based (TTL) – Assign a fixed expiration to a cache entry. Simple to configure but can serve outdated data until the timer expires.
- Event-driven – Hook into your CMS or any data source that emits a webhook when content changes. The webhook triggers a purge of the related cache entry.
- Tag-based – Attach a logical tag to a group of fetches (e.g.,
product-list). When any item in that group changes, a single call torevalidateTag('product-list')clears every entry sharing the tag.
Mixing these approaches lets you balance freshness against cache-hit rates.
The hierarchy that works for most teams
- Browser cache – Static assets (CSS, JS, images) get a long
max-ageso the user’s device never asks the network again. - CDN – Edge nodes cache full HTML pages and API JSON, honoring
Cache-Controlheaders you set in Next.js. - Reverse proxy – An Nginx or Varnish instance sits in front of the Next.js server, serving cached responses for routes that rarely change.
- Next.js internal cache – The framework’s data and route caches handle per-request memoization and ISR.
- Application cache – Redis stores the results of heavy database queries, keyed by query parameters or tags.
- Database – The ultimate source of truth, queried only on a cache miss at every higher level.
When a request arrives, it walks down this list until a layer answers it. The fastest answer wins, and the response writes back up the chain for future hits.
Putting the pieces together
- Set
Cache-Controlheaders in your API routes - Configure your CDN – Enable edge caching for HTML and JSON endpoints, and make sure the CDN respects
Cache-Control. - Deploy a reverse proxy – Put Nginx in front of your Next.js server.
- Add Redis – Cache results of expensive database queries.
- Use
revalidateTagin your components – CallrevalidateTagto clear the Next.js internal cache for a specific tag.
Takeaway
A single cache inside Next.js speeds up the first request, but true scalability comes from a disciplined stack: browser → CDN → reverse proxy → framework → Redis → database. Configure each layer, handle invalidation with time, events, and tags, and watch your metrics. Latency stays low while your backend survives traffic spikes.
