Handbooks / Next.js / Chapter 10
Cookies, Headers and Runtimes
7 pages · ~5 min✓ Reviewed
Builds on Testing and Monitoring. Next up: Database Access and Internationalization.
cookies() and headers() Mental Model
1. What They Are
- Both come from
next/headersand are async: they return Promises - Request-time APIs: they read the incoming HTTP request
- Work in Server Components, Route Handlers, Server Actions, middleware/proxy
- Never in Client Components
import { cookies, headers } from 'next/headers' const store = await cookies() const theme = store.get('theme')?.value const ua = (await headers()).get('user-agent')
always await first
2. The Two Return Values
| cookies() | headers() | |
|---|---|---|
| Returns | cookie store | read-only Web Headers |
| Methods | get getAll has set delete | get(name) |
| Missing | get gives undefined | get gives null |
store.get(name)returns{ name, value }or undefined: use ?.value- Header names are case-insensitive
- You cannot set response headers through
headers()
3. Read vs Write
| Where | Read | Write cookies |
|---|---|---|
| Server Component | yes | no |
| Route Handler | yes | yes |
| Server Action | yes | yes |
Reading works everywhere on the server. Writing cookies only in Server Actions and Route Handlers.
Where You Can Read and Set Cookies
1. Who Can Write
| Read | Set | Delete | |
|---|---|---|---|
| Server Component | yes | throws | throws |
| Server Action | yes | yes | yes |
| Route Handler | yes | yes | yes |
2. Why Components Cannot Set
- Stream startsheaders sent
- Component renderstoo late
- set() throwsno Set-Cookie
Dynamic Rendering Triggered by Request APIs
1. What makes a route dynamic
cookies()orheaders()opts the route into dynamic rendering- Renders per request, not prerendered at build
- Request-time APIs: the whole route goes dynamic
| Trigger | Result |
|---|---|
cookies() / headers() | dynamic |
searchParams | dynamic |
params, no generateStaticParams | rendered on first request, then cached |
fetch with no-store | dynamic |
2. Isolate with Suspense
- With
cacheComponents, request APIs outside Suspense fail the build - Wrap only the cookie reader; the rest stays static
Edge vs Node.js Runtime
1. Setting the Runtime
- Set per route with an exported constant
- Node.js is the default
cookies()andheaders()work in both: same Web-standard request and response objects
export const runtime = 'edge' // or export const runtime = 'nodejs'
2. Edge vs Node.js
| Edge | Node.js | |
|---|---|---|
| Engine | V8 isolates | Full Node |
| APIs | Web only: fetch, Request, Response, Headers, crypto.subtle | fs, net, child_process, Buffer, native modules |
| Strengths | Fast cold start, global, small bundle limits | Most npm packages, TCP database drivers |
| Cannot | fs, native addons, many ORMs, TCP clients, ISR |
Common Mistakes
1. Await, Render and Dynamic
- No
await:cookies()is a Promise, sostore.getis not a function (TypeError) cookies().set()in a Server Component or during render throws- Move writes to a Server Action or Route Handler
cookies()in a layout or page makes the whole route dynamic: static caching is lost
const bad = cookies().get('s')?.value // Promise const store = await cookies() const ok = store.get('s')?.value
await first, then read with ?.value
- Isolate the read in a small component inside
<Suspense>
2. Auth Cookie Flags
'use server' (await cookies()).set('session', token, { httpOnly: true, secure: true, sameSite: 'lax', path: '/' })
set in a Server Action
httpOnly: hides it from JavaScriptsecure: sent over HTTPS onlysameSite: blocks cross-site CSRF
Cheat Sheet
1. Import and read
import { cookies, headers } from 'next/headers' const theme = (await cookies()).get('theme')?.value const ua = (await headers()).get('user-agent')
get returns { name, value } or undefined
Without await, you hold a Promise; store.get is not a function (TypeError).
2. Write and dynamic rendering
| Where | Read | Set cookie |
|---|---|---|
| Server Component | yes | throws |
| Server Action | yes | yes |
| Route Handler | yes | yes |
- Using either API makes the whole route dynamic
- Wrap the reading component in
<Suspense>to limit the dynamic part
Check yourself
Spot the bug: const store = cookies(); const t = store.get('theme')?.value;
storeis a Promise, not a cookie store, so.getis not available on it.- Fix:
const store = await cookies().
A Server Component calls (await cookies()).set('visited', '1'). What happens, and where should it move?
- It throws: the response is already streaming, so
Set-Cookiecan no longer be sent. - Move it to a Server Action or Route Handler.
A layout reads cookies() to pick a theme. Which pages stay static?
- None under it: the whole route becomes dynamic, rendered per request.
- Isolate the read in a component wrapped in
<Suspense>.