Your Next.js middleware calls response.cookies.set('theme', 'dark') on the NextResponse it returns, but the Server Component rendering that same request still reads the old value from cookies(). Why, and what do you do instead?
answer
- request cookies are the past
- response cookies are the future
- nothing crosses without the browser
- forward inward or redirect and replay
- only the returned response is sent
basics
~20 sSetting a cookie on the response only writes a Set-Cookie header on the way out; the request already in flight still carries what the browser sent. Either forward the value inward as a request header on NextResponse.next, or redirect so the browser replays the request with the new cookie.
solid answer
~50 s`response.cookies.set()` writes a `Set-Cookie` header on the response you are about to send. That is a *future* instruction to the browser — it changes what arrives on the next request, not what arrived on this one. Meanwhile `cookies()` in a Server Component reads the request the browser actually sent, so it still reports the old value. You have two clean fixes. If the render just needs the value, forward it inward: put it on a cloned `Headers` and return `NextResponse.next({ request: { headers } })`, then read that header downstream while still setting the cookie on the response so the browser persists it. If the render genuinely must run with the cookie in place, return `NextResponse.redirect()` with the cookie set on that response — the browser stores it and replays the navigation, and the second pass sees it normally. And remember only the response you actually return is sent: cookies set on a `NextResponse.next()` you then discard vanish.
code
typescript · 13 linesimport { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const theme = request.cookies.get('theme')?.value ?? 'dark'
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-theme', theme)
const response = NextResponse.next({ request: { headers: requestHeaders } })
response.cookies.set('theme', theme, { path: '/' })
return response
}go deeper
Know that request.cookies reads what the browser sent and response.cookies writes Set-Cookie for later, and that get() returns an object whose .value you need.
Explain why the current render cannot see a cookie middleware just set: the value only crosses from response to request by way of the browser's next request.
Pick the right remedy under pressure — forward the value as a request header when the render only needs to know it, redirect and replay when code you do not control must read cookies() — and make the redirect conditional so it cannot loop.
Set the house rule that middleware decides for this request and persists for the next, and make sure no shared component silently depends on a cookie written earlier in the same request, since that dependency fails only on first visits.
## The two cookie surfaces in middleware `NextRequest` and `NextResponse` each expose a cookie API, and they are not two views of one thing: - `request.cookies` — a read view over the `Cookie` header the browser sent. `request.cookies.get('theme')` returns a `{ name, value }` object (so you want `?.value`), and `getAll()`, `has()` and `delete()` are there too. - `response.cookies` — a *writer* over the `Set-Cookie` headers of the response you are constructing. `response.cookies.set('theme', 'dark', { path: '/', maxAge: 60 * 60 * 24 })` appends an instruction for the browser. The asymmetry is the whole answer. One reflects the past, the other schedules the future. ## Why the Server Component sees the old value When middleware finishes, Next resolves and renders the route for the request that arrived. `cookies()` from `next/headers` reads that request. The `Set-Cookie` header you attached is sitting on the response object, waiting to be flushed to the client at the end. The browser has not seen it, has not stored it, and certainly has not re-sent it. So the render sees exactly what the browser sent — the old value, or nothing at all. This reads as a bug because the code looks sequential: set, then render. But there is no shared cookie jar on the server; there is an inbound request and an outbound response, and the cookie only crosses from one to the other by going through the browser. ## Fix 1 — forward the value inward When the render just needs to *know* the value, hand it in as a request header while still setting the cookie for future requests: ```ts const theme = request.cookies.get('theme')?.value ?? 'dark' const requestHeaders = new Headers(request.headers) requestHeaders.set('x-theme', theme) const response = NextResponse.next({ request: { headers: requestHeaders } }) response.cookies.set('theme', theme, { path: '/' }) return response ``` This renders correctly on the very first visit and persists the choice for later ones. The route reads `x-theme` rather than the cookie, so make that explicit in the code — a page that reads a cookie on some requests and a header on others is a debugging trap. ## Fix 2 — redirect and let the browser replay When the render must genuinely happen with the cookie present — because deep code you do not control reads `cookies()` — set it on a redirect response instead: ```ts const response = NextResponse.redirect(request.nextUrl) response.cookies.set('theme', 'dark', { path: '/' }) return response ``` `Set-Cookie` is honoured on a redirect response, so the browser stores the cookie and then issues the follow-up request carrying it. The cost is an extra round trip, and you must guarantee the redirect is not re-triggered on the replay — gate it on the cookie being absent, or the user gets a redirect loop. That loop is the classic failure of this pattern and the reason to prefer fix 1 whenever the render only needs the value. ## Only the returned response is sent A related trap: ```ts const response = NextResponse.next() response.cookies.set('theme', 'dark') // ...later return NextResponse.redirect(url) // the cookie is gone ``` Headers and cookies live on a specific `NextResponse` instance. Whatever you return is what gets serialised; anything you decorated on an object you then abandoned is discarded silently. Build the response you intend to return first, decorate it, and return that one — or, when branching, set cookies at the end on whichever object won. ## Deleting behaves the same way `response.cookies.delete('theme')` writes an expiry instruction, so the same rule holds: the current render still sees the cookie, and it disappears from the *next* request. If you are clearing state and then rendering an empty view, forward that fact inward or redirect — do not assume the delete took effect mid-request. ## What this generalises to Any state middleware writes to the client — cookies above all — is asynchronous with respect to the render it precedes. Middleware can *decide* things for this request and *persist* things for the next one, and conflating the two is the single most common middleware bug in Next apps. When you catch yourself writing "set it and then read it back", stop and pick one of the two mechanisms above.
- Does a cookie set on a redirect response actually reach the browser?Yes. `Set-Cookie` is honoured on a 3xx response, so the browser stores it and includes it on the follow-up request to the Location target. That is what makes the redirect-and-replay pattern work — and why the redirect must be conditional, or the replay triggers it again and you get a loop.
- You build a NextResponse.next(), set a cookie on it, then return a NextResponse.redirect() instead. What happens to the cookie?It is lost. Cookies and headers belong to the specific NextResponse instance you decorated, and only the object you actually return gets serialised. Decide which response you are returning first, then set cookies on that one.
- Does the same timing problem apply to response.cookies.delete()?Yes. Deleting writes an expiry instruction into the outgoing response, so the browser drops the cookie before its *next* request. The render happening now still sees the old cookie, so if the view depends on it being gone, forward that fact as a request header or redirect.
saying these in an interview costs you the question
- Assumes response.cookies.set is visible to cookies() in the same render
- Thinks the server keeps a per-request cookie jar both sides share
- Sets cookies on a response object that is never returned
- Redirects unconditionally after setting the cookie, creating a loop
- Forgets request.cookies.get returns an object, not the string value