How to Fix: Cookies are not immedietly set when setting them in middleware, and reading them in page

6 min read

Next.js middleware cookies are not visible immediately in the same request

If you set a cookie in middleware and then try to read it in the page during that same request, the value can appear missing. This is not a random race condition. It happens because middleware modifies the outgoing response, while your page usually reads from the incoming request snapshot. In other words, the cookie is scheduled to be sent to the browser, but it has not yet come back on a new request.

Understanding the Root Cause

In Next.js middleware, calling response.cookies.set(...) writes a Set-Cookie header onto the response that will be sent back to the browser. However, your page, layout, server component, or route handler often reads cookies from the request context, not from that response header.

That means there are really two different states involved:

  • Incoming request cookies: what the browser already sent.
  • Outgoing response cookies: what middleware wants the browser to store next.

The page rendered during the same request can only reliably see the incoming request cookies. The newly set cookie becomes available after the browser receives the response and makes another request.

This is why authentication flows often fail when implemented like this:

  1. Middleware checks auth state.
  2. Middleware sets a cookie.
  3. Page immediately expects that cookie to exist during the same render.

From a protocol perspective, this is expected behavior. HTTP cookies are client round-trip state. A server response cannot retroactively change the request that is already being processed.

In App Router projects, this can feel surprising because middleware, server components, and route handlers all run on the server, but they still operate on different request/response phases.

Step-by-Step Solution

The safest fix is to structure the flow so the cookie is read on a subsequent request, or avoid depending on the cookie immediately after setting it in middleware.

This is the most common and reliable solution. Middleware sets the cookie, then redirects the user. The redirected request includes the new cookie, and the page can read it normally.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const hasSession = request.cookies.has('session')

  if (!hasSession && request.nextUrl.pathname === '/protected') {
    const loginUrl = new URL('/bootstrap-auth', request.url)
    const response = NextResponse.redirect(loginUrl)

    response.cookies.set('session', 'example-token', {
      httpOnly: true,
      secure: true,
      sameSite: 'lax',
      path: '/',
    })

    return response
  }

  return NextResponse.next()
}

Then in your page or server component:

import { cookies } from 'next/headers'

export default function ProtectedPage() {
  const cookieStore = cookies()
  const session = cookieStore.get('session')

  return (
    <div>
      {session ? 'Logged in' : 'Not logged in'}
    </div>
  )
}

Because the browser performs a new request after the redirect, the cookie is now part of the request headers.

Option 2: Move the auth bootstrap into a route handler

If middleware is only being used to create or refresh auth state, consider using a dedicated route handler that sets the cookie and then redirects.

import { NextResponse } from 'next/server'

export async function GET() {
  const response = NextResponse.redirect(new URL('/protected', 'http://localhost:3000'))

  response.cookies.set('session', 'example-token', {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
  })

  return response
}

This pattern is cleaner when login, token exchange, or session initialization is part of a discrete flow.

Option 3: Pass temporary state through the URL or headers

If you must make a decision during the same lifecycle, do not depend on the newly set cookie. Instead, pass state directly through a redirect URL parameter, internal header, or server-side lookup mechanism.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const response = NextResponse.next({
    request: {
      headers: new Headers(request.headers),
    },
  })

  response.headers.set('x-auth-bootstrap', 'true')
  response.cookies.set('session', 'example-token', {
    httpOnly: true,
    path: '/',
  })

  return response
}

However, for auth decisions that need persistence, a redirect-based cookie round trip remains the best approach.

If your real goal is to know whether the user is logged in:

  1. Check the existing session cookie in middleware.
  2. If missing or expired, redirect to a route that creates or refreshes the session.
  3. Set the cookie there.
  4. Redirect back to the protected page.
  5. Read the cookie on the next request.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const session = request.cookies.get('session')
  const pathname = request.nextUrl.pathname

  if (!session && pathname.startsWith('/dashboard')) {
    const url = new URL('/auth/start', request.url)
    url.searchParams.set('returnTo', pathname)
    return NextResponse.redirect(url)
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*'],
}

Then your auth route:

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  const returnTo = request.nextUrl.searchParams.get('returnTo') || '/dashboard'

  const response = NextResponse.redirect(new URL(returnTo, request.url))

  response.cookies.set('session', 'new-session-value', {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
  })

  return response
}

This keeps the flow aligned with how HTTP response cookies actually work.

Common Edge Cases

1. Secure cookies on localhost

If you set secure: true during local HTTP development, the browser may ignore the cookie. Use:

secure: process.env.NODE_ENV === 'production'

2. Path mismatch

If the cookie path is too narrow, the next request may not include it. For app-wide auth cookies, use path: '/'.

3. SameSite restrictions

When the cookie is set during cross-site login flows, SameSite configuration matters. lax is common, but some external auth providers may require different handling.

4. Reading cookies in static rendering contexts

If a page is statically optimized or cached unexpectedly, you may think the cookie is missing when the real problem is rendering mode. Make sure the route is dynamic when relying on request-time cookies.

5. Middleware is not the best place for full auth logic

Middleware is great for lightweight checks, redirects, rewrites, and guards. It is usually not the ideal place for complex token refresh flows unless the design clearly accounts for the response/request boundary.

6. Client-side code expects immediate availability

If you navigate in the browser and expect a server-set cookie to be visible before the round trip completes, the UI can appear inconsistent. Trigger a redirect, refresh, or follow a route-driven auth bootstrap pattern.

7. Domain mismatch

If the cookie domain does not match your app host, the browser will not send it back. This often shows up in multi-subdomain setups.

FAQ

Not reliably during the same request. Middleware sets the cookie on the response, while the page usually reads cookies from the request. You need a new request, typically through a redirect.

Why does this feel inconsistent in Next.js App Router?

Because multiple server-side primitives exist together, it can look like shared state should update instantly. But request cookies and response cookies are still separated by the HTTP lifecycle.

What is the best pattern for protected routes?

Use middleware only to detect whether access should continue. If a session must be created or refreshed, redirect to a route handler, set the cookie there, and redirect back so the protected page reads the cookie on the next request.

For the original issue, the core fix is simple: do not expect a cookie set in middleware to be readable in the page during the same request. Treat the cookie as available only after the browser completes the response cycle and makes a fresh request.

Leave a Reply

Your email address will not be published. Required fields are marked *