Next.js Deep Dive — 03 · Production Next.js
Shipping a Next.js app is easy; running one for years is a different skill. Between March 2025 and August 2026 the framework and its React Server Components layer produced an authorisation bypass that ignored middleware entirely, a CVSS 10.0 unauthenticated remote code execution in the RSC protocol, a run of follow-up denial-of-service and source-exposure bugs, and critical fixes in the image optimiser's native dependencies. Teams that had layered defences, a patch process, and observability shrugged; teams whose only auth check lived in middleware, or who ran a year-old minor, did not. This post covers authentication patterns that survive a bypass, the incidents themselves and what each teaches, next/image and next/font in production, bundle analysis, self-hosting with output: 'standalone' and Docker versus a managed platform, instrumentation.ts with OpenTelemetry, and environment-variable leaks. It ends with the planned extensions of this series.
Series map: 00 App Router Mental Model → 01 Data Fetching, Caching & Server Actions → 02 Rendering Strategies & Runtimes → 03 Production Next.js (this post) (future: see the extensions table below).
Learning Outcome
- Design authentication in layers (Proxy, Data Access Layer, per-page and per-action checks) so that no single bypass exposes data.
- Explain CVE-2025-29927 and CVE-2025-55182 (React2Shell): mechanism, affected versions, fixed versions, and the lessons.
- Run a patch process that matches the Next.js LTS policy.
- Configure
next/imagesafely for Next.js 16, and usenext/fontwithout layout shift or third-party requests. - Find client-bundle bloat with the bundle analyzer and fix it at the server/client boundary.
- Self-host with
output: 'standalone'in Docker, including shared caching, the Server Actions key, version skew and graceful shutdown. - Instrument the server with
instrumentation.ts,onRequestErrorand OpenTelemetry. - Prevent secret leaks through
NEXT_PUBLIC_,next.configenv, props and action return values.
Chapter 1 — Authentication in layers
Terminology
- Authentication — who is the caller? (a valid session)
- Authorisation — may this caller do this to this resource? (ownership, role)
- Optimistic check — a cheap check on the presence of a session cookie, without a database or full verification; good for redirects, never enough for data.
- Secure check — full session verification plus an authorisation decision, next to the data access.
- IDOR — Insecure Direct Object Reference: the caller changes an id and gets someone else's record because only authentication was checked.
Where each check belongs
| Layer | Check | Purpose | Enough on its own? |
|---|---|---|---|
proxy.ts |
Optimistic (cookie present?) | Redirect logged-out users early; cheap | No: bypassable, and skips any path its matcher excludes |
| Layout | Avoid auth logic here | A shared layout is not re-rendered on navigation between its children (partial rendering) | No |
| Page / Server Component | Secure, via the DAL | Decide what UI to render | For that page's reads |
| Server Action | Secure, inside the action | Actions are public POST endpoints | For that mutation |
| Route Handler | Secure, inside the handler | Public HTTP endpoint | For that endpoint |
| Data Access Layer | Secure + authorisation per query | The one place data is read or written | The real boundary |
The DAL from Post 01, extended with a verified session that is memoised per request:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// data/session.ts
import 'server-only'
import { cache } from 'react'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { decrypt } from '@/lib/session-crypto' // verifies signature and expiry
export const verifySession = cache(async () => {
const token = (await cookies()).get('session')?.value
const payload = token ? await decrypt(token) : null
if (!payload?.userId) redirect('/login')
return { userId: String(payload.userId), role: String(payload.role) }
})
Every DAL function starts with await verifySession() and scopes its query to the caller (where: { id, ownerId: userId }). A page, an action and a Route Handler then get the same guarantees without remembering to write them.
Sessions
- Stateless (a signed and encrypted token in a cookie): no session store, but revocation before expiry needs extra machinery (short lifetimes, a deny-list).
- Database sessions (an opaque id in the cookie, state server-side): revocable instantly, one lookup per request (cheap with
React.cache). - Either way the cookie is
HttpOnly,Secure,SameSite=Lax(orStrict), with a sensibleMax-Age, and is set or deleted only in Server Actions or Route Handlers: setting cookies during render is not allowed.
Prefer a maintained auth library over hand-rolled crypto; the layering above applies whichever library you choose.
Auth with Cache Components
Under cacheComponents (Post 02), per-user data must never sit in a 'use cache' scope keyed only by public inputs. Read the session outside the cached scope and pass the user id in as an argument, so it becomes part of the key; or keep per-user parts as uncached dynamic holes inside Suspense. Since 15.1, forbidden() and unauthorized() (behind experimental.authInterrupts, still experimental as of 16.3) render forbidden.tsx / unauthorized.tsx with 403 / 401 status, as long as they run before streaming starts.
Common pitfall: "the dashboard layout checks the session, so all dashboard pages are safe." The layout's check runs on the first load; on soft navigation between child pages the layout is not re-rendered, and the child pages' actions are separate endpoints. Check in the DAL.
Chapter 2 — The incidents, and what they teach
CVE-2025-29927: middleware bypass (March 2025)
Next.js used an internal header, x-middleware-subrequest, to stop middleware from recursing into itself. The server trusted that header on incoming requests, so an attacker who sent it with the right value made the server skip middleware entirely: authentication redirects, authorisation checks, security headers, everything.
- Severity: critical, CVSS 9.1.
- Affected: self-hosted apps using
next start(includingoutput: 'standalone'). Vercel, Netlify, Cloudflare Workers and static exports were not affected (Vercel's routing is decoupled from the Next.js server; exports have no server). - Fixed in: 15.2.3, 14.2.25, and backports 13.5.9 and 12.3.5.
- Timeline: reported late February 2025, patched March 17–18, disclosed March 21.
- Workaround: strip
x-middleware-subrequestat the load balancer or reverse proxy before it reaches Next.js.
Lesson: an app whose only authorisation lived in middleware had none. Apps that re-checked in the DAL lost a redirect, not their data.
CVE-2025-55182 "React2Shell": RSC remote code execution (December 2025)
A deserialisation flaw in how React decodes payloads sent to Server Function endpoints let a crafted HTTP request execute code on the server, without authentication.
- Severity: critical, CVSS 10.0. Reported November 29, disclosed December 3, 2025 (reporter: Lachlan Davidson).
- Affected React packages:
react-server-dom-webpack,react-server-dom-parcel,react-server-dom-turbopackat 19.0, 19.1.0, 19.1.1 and 19.2.0; fixed in 19.0.1, 19.1.2 and 19.2.1. Frameworks affected included Next.js, React Router, Waku,@parcel/rsc,@vitejs/plugin-rscand rwsdk. - Not only apps with actions: React's advisory states that an app without any Server Functions of its own may still be vulnerable if it supports React Server Components.
- Next.js impact (tracked as CVE-2025-66478): App Router apps on 15.x, 16.x, and 14.3.0-canary.77 or later canaries. Next.js 13.x, 14.x stable, the Pages Router and the Edge runtime were not affected.
- Next.js fixed versions: 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, 16.0.7. No workaround; upgrade was required.
npx fix-react2shell-nextperformed the version bumps. - Exploitation: exploited in the wild within days; AWS reported China-nexus threat groups using it.
- Secrets: Vercel advised that any app online and unpatched as of December 4, 2025, 1:00 PM PT should rotate its secrets, most critical first.
The follow-ups (December 2025 – January 2026)
Fixing a deserialiser invites a closer look at it:
| CVE | Type | CVSS | Note |
|---|---|---|---|
| CVE-2025-55184 | Denial of service (infinite loop on a crafted request to a Server Function endpoint) | 7.5 | Disclosed Dec 11, 2025 |
| CVE-2025-55183 | Source code exposure of Server Functions that stringify their arguments | 5.3 | Hardcoded secrets in source could leak; runtime process.env values were not affected |
| CVE-2025-67779 | Denial of service | 7.5 | The first DoS fix was incomplete |
| CVE-2026-23864 | Denial of service (crashes, out-of-memory, CPU) | 7.5 | Disclosed Jan 26, 2026 |
Only 19.0.4, 19.1.5 and 19.2.4 (and later) of the react-server-dom-* packages are safe from all of them. None of these enabled remote code execution.
2026: a monthly rhythm
Next.js security releases became regular events in 2026, on both supported lines (16.x Active LTS, 15.5.x Maintenance LTS). The July 2026 release (16.2.11 / 15.5.21) fixed 4 high and 5 medium issues. The August 2026 release (16.3.3 / 15.5.24, August 25) fixed two critical ones: unauthenticated remote code execution in the Image Optimization API through the libheif library used by sharp when optimising an attacker-controlled AVIF image (the patch disables AVIF optimisation until the upstream fix propagates), and an unauthenticated remote code execution on Windows-hosted servers running both routers without Cache Components (CVE-2026-75604).
The timeline, with fixed versions:
Lessons that transfer
| Lesson | Practice |
|---|---|
| Defence in depth | Auth in the DAL; Proxy only as an optimisation |
| Patch speed is a security control | Track the release line; automated dependency PRs; a documented "patch within 24 h" path for critical advisories |
| Know the support window | Next.js LTS: the current major is Active LTS until the next major; the previous major gets Maintenance LTS (critical and security fixes) for 2 years from its release. As of 2026: 16.x Active, 15.x Maintenance. 14.x is outside the policy. |
| Rotate after exposure | A remote code execution window means every secret the process could read is suspect |
| Shrink the attack surface | Disable features you do not use: images.unoptimized if you do not need the optimiser, no unused Route Handlers, no 'use server' exports nobody calls |
| A WAF rule is a stopgap | Vendors shipped rules within hours for React2Shell; the fix was still the upgrade |
| Keep secrets out of source | CVE-2025-55183 exposed function source, not process.env |
This blog runs Next.js 14.2 as a static export: no server process, so none of the server-side issues above apply. It still sits outside the LTS window, which matters the day it gains a server.
Chapter 3 — next/image and next/font
next/image
<Image> wraps <img> with: lazy loading by default, a srcset generated from width lists, format negotiation through the Accept header (formats, default ['image/webp']; add 'image/avif' to prefer AVIF), and required width/height (or fill) so the browser reserves space and avoids layout shift.
1
2
3
4
5
6
7
<Image
src={hero} // static import: hashed, cached forever (immutable)
alt="Dashboard overview"
sizes="(max-width: 768px) 100vw, 50vw" // without sizes the srcset only offers 1x/2x
preload // 16: replaces the deprecated `priority`
/>
Default width lists: deviceSizes [640, 750, 828, 1080, 1200, 1920, 2048, 3840] and imageSizes [32, 48, 64, 96, 128, 256, 384].
Next.js 16 changed several defaults, mostly for security:
| Setting | Before 16 | 16 |
|---|---|---|
qualities |
Any 1–100 | [75]; other quality values are coerced to the nearest allowed value |
minimumCacheTTL |
60 s | 14,400 s (4 h) |
imageSizes |
included 16 |
16 removed |
priority prop |
Supported | Deprecated in favour of preload |
Local src with a query string |
Allowed | Requires images.localPatterns (prevents enumeration) |
| Optimising images from local IPs | Allowed | Blocked unless dangerouslyAllowLocalIP |
| Redirects when fetching remote images | Unlimited | maximumRedirects 3 |
images.domains |
Supported | Deprecated: use remotePatterns |
Production notes:
- The optimiser is a server endpoint that fetches and decodes untrusted images: it is SSRF, CPU and native-library attack surface (the August 2026 AVIF issue). Restrict
remotePatternsto exact hosts and paths. dangerouslyAllowSVGserves SVG through the optimiser; pair it with acontentSecurityPolicyfor images. Since 15,contentDispositionTypedefaults toattachment.- There is no API to invalidate optimised images; they live under
<distDir>/cache/imagesuntil the TTL runs out. Change thesrcto bust them. - On glibc Linux,
sharpmay need allocator tuning to avoid excessive memory; Next.js 15+ usessharpautomatically withnext startand standalone output. - A static export has no optimiser: set
images.unoptimized(this blog does) or supply a customloaderthat points at an image CDN.
next/font
next/font downloads Google Font files (or uses your local files) at build time and serves them with your static assets, so the browser never contacts Google. It generates a fallback @font-face with adjusted metrics (adjustFontFallback, default true for Google fonts and 'Arial' for local fonts) to reduce layout shift while the real font loads, uses display: 'swap' by default, and preloads the font on the routes that use it (preload default true; the subsets you list are what gets preloaded).
1
2
3
4
5
6
7
8
// app/fonts.ts
import { Inter, JetBrains_Mono } from 'next/font/google'
export const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
export const mono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' })
// app/layout.tsx
// <html lang="en" className={`${inter.variable} ${mono.variable}`}>
Call each font loader once in a definitions module and import the object where needed; every call creates a separate hosted instance. A font used in a page is preloaded only on that route, one used in the root layout on all routes.
Chapter 4 — Bundle analysis
Tools
next buildoutput lists every route with its size, the first-load JS, and the rendering mode symbol (Post 02).- Next.js Bundle Analyzer (experimental since 16.1, Turbopack):
next experimental-analyzeopens an interactive view of client and server bundles, filterable by route, showing the full import chain for any module, including across the server-to-client boundary and dynamic imports. @next/bundle-analyzer: the webpack-era plugin, still useful for projects building with--webpack.
What to look for
| Finding | Usual cause | Fix |
|---|---|---|
| A large library in first-load JS | Imported from a 'use client' file for one small use |
Do the work in a Server Component; pass the result as a prop |
| The same library in many route chunks | Barrel file (index.ts re-exporting everything) |
Import from the specific module; experimental.optimizePackageImports for known packages |
| A heavy widget nobody sees above the fold | Eager import | next/dynamic / import() behind interaction or visibility |
| A server-only package in the client graph | Accidental import through shared utils | import 'server-only' in the module; split the file |
| Huge RSC payload rather than JS | Large props to Client Components | Pass DTOs, not rows (Post 00) |
| Slow server cold starts | Enormous server bundle | Check the server view of the analyzer; serverExternalPackages for packages that should stay unbundled |
Trick: measure before and after with the route table from
next build, not with dev-mode network numbers. Development bundles are unminified and compiled on demand.
Chapter 5 — Self-hosting vs a managed platform
Standalone output
output: 'standalone' uses output file tracing (@vercel/nft) to copy only the files each route needs, including the needed parts of node_modules, into .next/standalone, with a minimal server.js that replaces next start. It does not copy public/ or .next/static, which are expected to go to a CDN; copy them in if the server should serve them. PORT and HOSTNAME set the listen address. In a monorepo, set outputFileTracingRoot so files outside the app folder are traced.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Dockerfile (multi-stage)
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# NEXT_PUBLIC_* values are inlined here, at build time
RUN npm run build
FROM node:22-alpine AS run
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
RUN addgroup -S app && adduser -S app -G app
COPY --from=build --chown=app:app /app/.next/standalone ./
COPY --from=build --chown=app:app /app/.next/static ./.next/static
COPY --from=build --chown=app:app /app/public ./public
USER app
EXPOSE 3000
CMD ["node", "server.js"]
Next.js 16 needs Node.js 20.9 or later; any current LTS base image satisfies that.
The multi-instance checklist
One container works out of the box. Two or more need all of these:
| Concern | Default | Multi-instance fix |
|---|---|---|
ISR / fetch cache |
Per-instance disk + 50 MB memory | cacheHandler pointing at shared storage; cacheMaxMemorySize: 0 |
'use cache' entries |
Per-instance memory LRU | cacheHandlers (for 'use cache: remote') backed by shared storage |
| Tag invalidation | Only on the instance that called it | Implement refreshTags() in the handler to sync tag state before each request |
| Server Actions closures | New key per build | Same NEXT_SERVER_ACTIONS_ENCRYPTION_KEY everywhere |
| Build identity | Random build ID | One image for all environments, or generateBuildId |
| Version skew in rolling deploys | Old clients hit new servers | deploymentId: assets get ?dpl=, navigations send x-deployment-id, a mismatch forces a hard reload |
| Streaming | Buffered by Nginx | X-Accel-Buffering: no; check the load balancer |
after() work |
Lost on abrupt kill | Send SIGTERM/SIGINT and allow a 10–30 s drain |
| Bad clients | Hit Node.js directly | A reverse proxy for slow connections, payload limits, rate limits |
Vercel, adapters, or your own
| Managed (Vercel) | Self-hosted Node.js / Docker | |
|---|---|---|
ISR, tag invalidation, 'use cache: remote' |
Built in | Your cache handler |
| Skew protection | Built in | deploymentId + your deploy strategy |
| Image optimisation | Platform service | sharp in your container |
| Proxy bypass (CVE-2025-29927) | Not affected | Affected until patched |
| React2Shell (CVE-2025-55182) | Platform WAF rules plus upgrade | Upgrade was the only fix |
| Cost model | Per request / usage | Per instance |
| Control and data residency | Platform's | Yours |
Next.js 16.2 made the Adapters API stable: a supported way for other platforms to hook into the build and consume its output, replacing reverse-engineered build output for providers such as OpenNext-based deployments.
Chapter 6 — instrumentation.ts and OpenTelemetry
instrumentation.ts in the project root (or in src/) is stable since Next.js 15. Its register() runs once when a new server instance starts, before your code. Its onRequestError() (added in 15) receives every error thrown on the server with context: which router, and whether it came from a Server Component, Server Action, Route Handler or Proxy.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// instrumentation.ts
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel({ serviceName: 'shop-web' })
}
export async function onRequestError(
err: unknown,
request: { path: string; method: string; headers: Record<string, string | string[] | undefined> },
context: { routerKind: string; routePath: string; routeType: string },
) {
await fetch(process.env.ERROR_SINK_URL!, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ message: String(err), path: request.path, method: request.method, ...context }),
})
}
Next.js is instrumented out of the box. The spans it emits by default include the root request span (next.span_type BaseServer.handleRequest), App Router rendering (AppRender.getBodyResult), each fetch (AppRender.fetch, disabled with NEXT_OTEL_FETCH_DISABLED=1), Route Handler execution (AppRouteRouteHandlers.runHandler), generateMetadata, and a zero-length start response span at first byte. Attributes include next.route (the route pattern, such as /[param]/user) and next.rsc (whether the request was an RSC request such as a prefetch). NEXT_OTEL_VERBOSE=1 emits more spans.
For a manual NodeSDK setup instead of @vercel/otel, import it only when process.env.NEXT_RUNTIME === 'nodejs', from a separate instrumentation.node.ts.
What to put on dashboards: p95 of start response (time to first byte) per next.route, error counts from onRequestError by routeType, fetch span durations per upstream host, and prefetch (next.rsc) volume after a navigation change.
Watch out: error payloads are a leak vector too. Do not ship full request headers (cookies,
authorization) to a third-party sink; pick the fields you need, as above.
Chapter 7 — Environment variables and NEXT_PUBLIC_ leaks
How loading works
Lookup order, stopping at the first hit: process.env, .env.$(NODE_ENV).local, .env.local (skipped when NODE_ENV is test), .env.$(NODE_ENV), .env. Variables can reference others with $VAR. Outside Next.js (an ORM config, a test runner), loadEnvConfig from @next/env loads them the same way.
The inlining rule
Only variables prefixed NEXT_PUBLIC_ reach the browser, and they are inlined as literals at next build. Consequences:
- One Docker image promoted from staging to production keeps staging's
NEXT_PUBLIC_values. Runtime values must be read on the server during dynamic rendering (await connection()first) or served by an endpoint. - Only direct references are replaced:
process.env[name]orconst env = process.env; env.NEXT_PUBLIC_Xare not inlined. - The
envkey innext.configinlines values into the bundle the same way. This blog uses it forNEXT_PUBLIC_BASE_PATH, which is public by nature.
How secrets actually leak
| Leak | Example | Prevention |
|---|---|---|
| Wrong prefix | NEXT_PUBLIC_STRIPE_SECRET_KEY |
Code review rule and a CI grep: no SECRET, TOKEN, KEY after NEXT_PUBLIC_ unless explicitly public |
next.config env |
env: { DB_URL: process.env.DB_URL } |
Never put secrets in env |
| Props to a Client Component | <Map apiKey={process.env.MAPS_SERVER_KEY} /> |
Props are in the RSC payload: pass results, not keys |
| Action return values | Returning a full user row or config object | Return minimal objects (Post 01) |
| Hardcoded in source | db.connect('postgres://user:pw@…') inside a Server Function |
Environment variables only; CVE-2025-55183 exposed function source |
| Logs and error sinks | Logging process.env or request headers |
Allow-list fields |
| Build artefacts | .env copied into a Docker image |
.dockerignore; secrets at runtime, not build time |
Structural defences: read process.env only inside the server-only DAL, mark those modules with import 'server-only', and enable experimental.taint so experimental_taintUniqueValue can block a specific secret string from ever being passed to the client.
Common pitfall: "it is only used on the server" is not the same as "it is not in the client bundle". A
NEXT_PUBLIC_reference in any module that a client module imports ships the literal, even if the code path never runs in the browser.
Practice Exercises
Exercise 1 — Survive a Proxy bypass
An app's only protection for /admin is a proxy.ts redirect when the role cookie is not admin. Describe an attack even without a framework CVE, and redesign the checks.
Solution
The cookie is client-controlled: set role=admin and the redirect passes. Even with a signed cookie, any bypass or matcher gap exposes everything. Redesign: Proxy only checks that a session cookie exists; verifySession() in the DAL decrypts and verifies the session and loads the role from the session store; every admin DAL function checks role === 'admin'; every admin Server Action calls those DAL functions.
Exercise 2 — Triage an advisory
A new critical advisory lists "Next.js 15.x and 16.x App Router, fixed in 15.5.x and 16.3.x". You run 15.2.4 self-hosted and a static-export marketing site on 14.2. What do you do for each?
Solution
15.2.4 is inside the affected range and on an old minor: upgrade to the latest 15.5.x patch (the Maintenance LTS line), deploy, and if the advisory is remote code execution and you were exposed, rotate secrets. Then plan the move to 16.x. The static export has no Next.js server, so server-side advisories do not apply; note that 14.x is outside the LTS policy and schedule an upgrade.
Exercise 3 — Fix the image config
A Next.js 16 upgrade makes every product image look slightly different and a designer complains quality={90} is ignored. Explain and fix.
Solution
In 16, images.qualities defaults to [75] and quality is coerced to the nearest allowed value, so 90 becomes 75. Add the needed values: images: { qualities: [75, 90] }. Keep the list short: each allowed quality is another variant attackers can request and the server must cache.
Exercise 4 — Two pods, stale pages
After scaling from one to two pods, users see a product page flip between old and new prices for minutes after an edit. List the causes and fixes.
Solution
Each pod has its own ISR and data cache on local disk and memory, and revalidateTag / updateTag on one pod does not reach the other. Configure a shared cacheHandler (and cacheHandlers for 'use cache: remote'), set cacheMaxMemorySize: 0, and implement refreshTags() so each pod syncs tag invalidations before serving. Also make sure both pods run the same build and share NEXT_SERVER_ACTIONS_ENCRYPTION_KEY.
Exercise 5 — Find the leak
A Server Component renders <Checkout config={config} /> where config = { currency: 'EUR', stripeSecret: process.env.STRIPE_SECRET_KEY, publishableKey: process.env.NEXT_PUBLIC_STRIPE_KEY }. What leaks, how would you detect it, and how do you prevent it structurally?
Solution
stripeSecret is serialised into the RSC payload and visible in page source, even if Checkout never renders it. Detect it by searching the HTML and _rsc responses for the key prefix, or with experimental_taintUniqueValue on the secret, which makes React throw when it is passed to the client. Prevent it by reading the secret only in the DAL, giving Checkout a narrow props type ({ currency, publishableKey }), and marking the config module server-only.
Exercise 6 — Instrument a slow route
Users report slow /products/[id] pages. Using only the default Next.js spans, how do you find whether the time goes to rendering, to upstream fetch calls, or before the first byte?
Solution
Filter traces by next.route = /products/[id]. Compare the root BaseServer.handleRequest duration with the start response span (time to first byte), the AppRender.getBodyResult render span, and the child AppRender.fetch spans grouped by host. Long fetch spans in sequence point to a waterfall (Post 01); a late start response with short fetches points to blocking work outside Suspense (Post 02).
Cheat-sheet
| Topic | Rule |
|---|---|
| Real auth boundary | The server-only DAL, called by every page, action and handler |
| Proxy | Optimistic checks only |
| Layout auth | Not re-run on child navigation; do not rely on it |
| CVE-2025-29927 | x-middleware-subrequest bypass; fixed 15.2.3 / 14.2.25 / 13.5.9 / 12.3.5 |
| CVE-2025-55182 | CVSS 10.0 RSC RCE; React 19.0.1 / 19.1.2 / 19.2.1; Next.js 15.0.5 … 16.0.7 |
| Safe from all follow-ups | react-server-dom-* 19.0.4 / 19.1.5 / 19.2.4 or later |
| LTS | Current major Active; previous major Maintenance for 2 years from release |
next/image in 16 |
qualities [75], minimumCacheTTL 4 h, preload replaces priority |
next/font |
Build-time self-hosting, display: 'swap', metric-adjusted fallback |
| Bundle analyzer | next experimental-analyze (16.1+, Turbopack) |
| Standalone | .next/standalone/server.js; copy public and .next/static |
| Multi-instance | Shared cache handler, refreshTags(), encryption key, deploymentId |
| Observability | register() + onRequestError(); @vercel/otel registerOTel |
NEXT_PUBLIC_ |
Inlined at build; frozen per image; only direct references |
What's next
This is the last planned post of the core series. The table below lists the extensions. For the underlying layers, the JavaScript Deep Dive series covers the event loop, promises and module semantics that streaming and Server Functions rely on; React Deep Dive covers Suspense, transitions, useActionState and the React Compiler that Next.js 16 supports as a stable option; Vue Deep Dive covers the Nuxt side of the same problems (server rendering, hybrid route rules, Nitro deployment) for teams choosing between the two.
References
- Next.js Docs — Authentication and Data Security
- Vercel — Postmortem on the Next.js middleware bypass (CVE-2025-29927)
- React — Critical security vulnerability in React Server Components (CVE-2025-55182)
- React — Denial of service and source code exposure in React Server Components
- Next.js — Security advisory CVE-2025-66478 and August 2026 security release
- Next.js — Support policy (LTS)
- Next.js Docs — Image component and Font module
- Next.js Docs — Self-hosting and
output - Next.js Docs — OpenTelemetry and Environment variables
- Next.js 16.1 (bundle analyzer) and 16.2 (Adapters stable)
Planned future extensions
| # | Topic | Scope |
|---|---|---|
| 04 | Testing Next.js | Unit-testing Server Components and actions, Proxy matcher tests, Playwright and the 16.3 instant() navigation helper |
| 05 | Internationalisation | Locale routing with root params (next/root-params, 16.3), generateStaticParams per locale, caching per locale |
| 06 | Multi-tenant SaaS | Tenant resolution in Proxy, per-tenant cache keys, isolation in the DAL, custom domains |
| 07 | Real-time features | Streaming responses from Route Handlers, Server-Sent Events, WebSockets beside Next.js |
| 08 | Migrating from the Pages Router | Incremental adoption, getServerSideProps to Server Components, data-fetching parity |
| 09 | Deep dive: cache handlers | Writing a Redis cacheHandler and cacheHandlers, refreshTags(), eviction and failure modes |
| 10 | Performance lab | Core Web Vitals on a real app: LCP images, INP and hydration cost, partial prefetching trade-offs |
| 11 | AI features in Next.js | Streaming model output through Server Functions and Route Handlers, timeouts, after() for logging |










