Type something to search...
Next.js cookies function

Next.js cookies function

Cookies are one of those primitives that seem simple until you actually need to read or write one from a server-rendered React tree. In a traditional Express app you'd reach into req.headers.cookie, parse it yourself, and call res.setHeader('Set-Cookie', ...) when you needed to write one back. Next.js replaces all of that with a single function — cookies, imported from next/headers — that gives you a typed, promise-based API for reading incoming cookies and writing outgoing ones, wherever your code happens to be running on the server.

The catch is that "wherever your code happens to be running" actually matters a lot here. Reading and writing cookies are not symmetric operations in the App Router's rendering model, and understanding why is the difference between a route that works and one that throws a confusing runtime error the first time you try to call .set() from the wrong place. This article walks through the full API, the read/write asymmetry, and the caching implications that the reference docs mention only in passing.

Why cookies Isn't Just req.cookies

In a Server Component, there is no req object the way there is in an Express handler — Next.js abstracts request-scoped data behind functions like cookies(), headers(), and draftMode() precisely so the same component code can run consistently whether it's being statically prerendered, dynamically rendered per-request, or streamed. cookies() is one of what the docs call "Request-time APIs" — its return value genuinely cannot be known until an actual HTTP request arrives, because it's reading data the browser sent along with that specific request.

That has a direct consequence: calling cookies() in a layout or page opts that route into dynamic rendering. Next.js can't prerender a page at build time if part of its output depends on data that only exists once a real request lands. This is worth internalizing early, because it's the single most common surprise developers hit — a page that was static suddenly becomes dynamic (and slower under load, if you're not caching around it) the moment someone adds a cookies() call to check a feature flag or locale preference.

The Basic Shape

// app/page.tsx
import { cookies } from "next/headers";

export default async function Page() {
  const cookieStore = await cookies();
  const theme = cookieStore.get("theme");
  return "...";
}

Notice the await. As of Next.js 15, cookies() is an async function — it returns a promise that resolves to the cookie store, not the store itself. If you're coming from a Next.js 14 codebase and see const cookieStore = cookies() without an await, that's legacy syntax that Next.js still tolerates for backwards compatibility right now, but it's deprecated and the official codemod (npx @next/codemod@canary next-async-request-api .) exists specifically to migrate this pattern across a codebase automatically. If you're starting fresh, always await it.

Reading Cookies: The Methods You Actually Use

Once you have the resolved cookie store, it exposes a small, focused API:

MethodReturnsWhat it does
get(name){ name, value } objectReturns a single cookie by name
getAll()Array of objectsReturns every cookie, or every cookie matching a name if you pass one
has(name)BooleanChecks existence without pulling the value
set(name, value, options)Writes an outgoing cookie (restrictions apply — see below)
delete(name)Removes a cookie (same restrictions as set)
toString()StringSerializes the store back to a raw cookie-header string

Reading is unrestricted — you can call get, getAll, has, and toString from any Server Component, at any depth in the tree, because you're just inspecting data the browser already sent in the request headers. This is the part of the API that "just works" the way you'd expect.

// Reading every cookie the browser sent
import { cookies } from "next/headers";

export default async function Page() {
  const cookieStore = await cookies();
  return cookieStore.getAll().map((cookie) => (
    <div key={cookie.name}>
      <p>Name: {cookie.name}</p>
      <p>Value: {cookie.value}</p>
    </div>
  ));
}

Writing Cookies: Where It Actually Gets Restrictive

Here's the part the docs mention but don't dwell on long enough: you cannot call .set() or .delete() from inside a Server Component's render. Only from a Server Function (a Server Action) or a Route Handler.

The reason isn't arbitrary — it's a direct consequence of how HTTP works. A cookie is set via a Set-Cookie response header, and HTTP does not allow you to add headers to a response after you've already started streaming its body. Server Component rendering in the App Router is fundamentally a streaming process — React starts sending HTML to the browser as soon as pieces of the tree are ready, well before the whole page has finished rendering. By the time a deeply nested Server Component executes, headers may already be long gone. Server Functions and Route Handlers, on the other hand, run to completion before Next.js decides what to send back, so there's still a window to attach Set-Cookie headers.

// app/actions.ts
"use server";

import { cookies } from "next/headers";

export async function setTheme(theme: string) {
  const cookieStore = await cookies();
  cookieStore.set("theme", theme, {
    httpOnly: true,
    secure: true,
    path: "/",
  });
}

If you try to call .set() inside a page or layout component, Next.js will throw at runtime rather than silently failing — which is the right call, but it does mean this is a mistake you'll only discover when the code path actually executes, not at build time. If you have logic that conditionally sets a cookie based on some derived state, double-check where that logic actually lives before you ship it.

The Options Object

set() accepts a full options object matching the standard cookie attributes:

OptionTypePurpose
expiresDateAbsolute expiration date
maxAgeNumber (seconds)Relative expiration — this is usually the one you want
domainStringWhich domain(s) the cookie is valid for
pathString (default '/')Scopes the cookie to a URL path prefix
secureBooleanHTTPS-only transmission
httpOnlyBooleanBlocks client-side JS (document.cookie) from reading it
sameSite'lax' | 'strict' | 'none' | BooleanCross-site request behavior
priority'low' | 'medium' | 'high'Browser eviction priority under storage pressure
partitionedBooleanOpts into CHIPS partitioned storage

path is the only option with a real default ('/'); everything else falls back to browser defaults if omitted. In practice, the two you'll reach for constantly are maxAge and httpOnly — the latter especially, since any cookie storing something sensitive (a session token, a signed auth cookie) should almost always be httpOnly: true so it's invisible to client-side scripts, which meaningfully reduces your exposure to XSS-based token theft.

Deleting a Cookie — Three Equivalent Approaches

The docs list three ways to remove a cookie, and it's worth knowing all three exist because you'll see all three in the wild:

"use server";
import { cookies } from "next/headers";

// 1. The direct method
export async function deleteA() {
  const cookieStore = await cookies();
  cookieStore.delete("name");
}

// 2. Overwrite with an empty value
export async function deleteB() {
  const cookieStore = await cookies();
  cookieStore.set("name", "");
}

// 3. Force immediate expiry
export async function deleteC() {
  const cookieStore = await cookies();
  cookieStore.set("name", "value", { maxAge: 0 });
}

.delete() is the clearest expression of intent and the one you should default to. It carries an extra restriction worth knowing: it only works if the deletion happens on the same domain (and, for wildcard domains, the exact matching subdomain) and the same protocol (HTTP vs HTTPS) that the cookie was originally set under. If you set a cookie on https://app.example.com and later try to delete it from a Route Handler running under a slightly different subdomain, it silently won't match — which is a genuinely annoying thing to debug, because there's no error, the deletion just doesn't take effect.

Server Actions and the Single-Roundtrip Model

One detail that's easy to miss: when you set or delete a cookie inside a function used as a Server Action (passed to a form's action prop, for instance), Next.js can return the updated UI and apply the cookie change in a single server round-trip. The existing UI isn't torn down and remounted — React effects that depend on server-derived data simply re-run with the fresh values. If you also need previously cached data to reflect the change (say, a cached page that reads the cookie to decide what to render), you still need to explicitly call revalidatePath or revalidateTag inside the same action — setting a cookie does not implicitly invalidate any cache entries on its own.

Cache Components and the Suspense Boundary

If your project has the Cache Components model enabled, there's an additional wrinkle worth knowing about: calling cookies() outside of a <Suspense> boundary will prevent that route from being prerendered at all, because Next.js has no way to produce a static shell around a value it fundamentally can't know ahead of time. The fix, in most cases, is to push the component that calls cookies() down into its own component and wrap it in <Suspense>, so the rest of the page can still be served as a static shell while just that piece streams in dynamically:

// app/page.tsx
import { Suspense } from "react";
import { ThemeToggle } from "./theme-toggle";

export default function Page() {
  return (
    <div>
      <StaticHeroContent />
      <Suspense fallback={<div>Loading preferences…</div>}>
        <ThemeToggle />
      </Suspense>
    </div>
  );
}
// app/theme-toggle.tsx
import { cookies } from "next/headers";

export async function ThemeToggle() {
  const cookieStore = await cookies();
  const theme = cookieStore.get("theme")?.value ?? "light";
  return <div data-theme={theme}>{/* ... */}</div>;
}

This is the same "push the dynamic thing to the leaves" pattern that shows up throughout the App Router's caching model — the goal is always to keep as much of the page static and cacheable as possible, and isolate the parts that genuinely need per-request data into their own Suspense-wrapped islands.

Common Mistakes

Forgetting the await. If cookieStore.get() throws or behaves unexpectedly, check whether you actually awaited cookies() first. TypeScript will usually catch this for you if you're strict about typing, but plain JavaScript projects can silently limp along with a promise object where a cookie store was expected.

Trying to set a cookie from a Server Component. If you get a runtime error about cookies only being settable in a Server Action or Route Handler, the fix is almost always to move that .set() call into a 'use server' function and trigger it from a form action or event handler, rather than trying to set it during render.

Assuming a cookie deletion propagates instantly across domains/subdomains. If a .delete() call seems to do nothing, check that the domain and protocol match exactly what the cookie was set under.

Reaching for cookies() when you only need to read data once at request time. If a value never needs to change per-user or per-session, storing it in a cookie and reading it via this API opts your route into dynamic rendering unnecessarily. Reserve cookies() for genuinely per-request, per-user state — auth sessions, feature-flag overrides, locale/theme preferences a user has explicitly set — not for anything that could just be a build-time constant or a use cache-wrapped fetch.

Key Takeaways

QuestionAnswer
Where can I read cookies?Anywhere on the server — any Server Component, any depth
Where can I write/delete cookies?Only inside a Server Function (Server Action) or a Route Handler
Why the restriction?HTTP can't add headers after the response starts streaming
Does calling cookies() affect rendering?Yes — it opts the route into dynamic rendering, and under Cache Components it blocks prerendering unless wrapped in Suspense
How do I delete a cookie?.delete(name), or .set(name, ''), or .set(name, value, { maxAge: 0 }) — all equivalent
Does setting a cookie in a Server Action refresh cached data?No — call revalidatePath or revalidateTag explicitly if it should

The cookies function looks like a small utility, but it sits right at the boundary between two very different execution contexts — the streaming render and the completed-request response — and that boundary is exactly why the read and write halves of the API behave so differently. Once that model clicks, the restrictions stop feeling arbitrary and start feeling like the only way this could reasonably work.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper