Type something to search...
Next.js headers

Next.js headers

Every incoming HTTP request carries a bundle of headers you never see in your component tree by default — the user-agent, the authorization token, whatever your CDN or reverse proxy tacked on, cookies (technically their own header, though Next.js gives you a separate API for those). Reading any of that from inside a Server Component means reaching into the request itself, and headers from next/headers is the sanctioned way to do it without threading a request object down through every layer of your component tree.

It's a small API on paper — one function, no parameters, a read-only return value — but it comes with a handful of behavioral details that matter more than the surface area suggests, especially around when it forces dynamic rendering and how it interacts with the newer Cache Components model. This article covers the full reference, plus the practical patterns and mistakes that come up once you start actually using it.

What headers Actually Gives You

headers is an async function you call from a Server Component (or from Route Handlers, though there you more commonly reach for the Request object directly). It returns a read-only Web Headers object — the same interface the Fetch API uses on both the client and server, so if you've ever inspected a Response.headers, this will feel familiar.

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

export default async function Page() {
  const headersList = await headers();
  const userAgent = headersList.get("user-agent");

  return <p>You're browsing with: {userAgent}</p>;
}

Note the await. This trips people up constantly, especially anyone coming from Next.js 13 or 14 muscle memory, so it's worth addressing before anything else.

Why It's Async (and Why That Changed)

Through Next.js 14, headers() was a synchronous function — call it, get the object back immediately, no await needed. Starting with the 15.0 release candidate, it became async, and Next.js expects you to await it (or consume it via React's use() hook if you're calling it in a context where await isn't convenient).

For backwards compatibility, older synchronous call sites don't immediately break — Next.js still lets you access it without await in some cases, but that behavior is deprecated and will eventually stop working. If you're maintaining a codebase that upgraded from an older major version, running the official codemod (npx @next/codemod@latest next-async-request-api .) will rewrite call sites for you rather than leaving you to hunt them down manually.

The reason for the change isn't cosmetic. Next.js's newer caching and rendering model needs to know, structurally, which parts of your render tree depend on request-specific data versus which parts can be computed once and reused. Making these APIs async lets the framework treat "reading the request" as an explicit, awaitable operation it can reason about — which sets up the dynamic-rendering behavior described next.

The Returned Object: What You Can and Can't Do With It

headers() takes no parameters and returns a standard Web Headers instance, so every method you'd expect from that interface is available:

  • get(name) — the one you'll use most; returns the value for a header name, or null if it isn't present.
  • has(name) — a boolean check, useful when you only care whether a header exists at all (a custom feature-flag header, for instance).
  • entries(), keys(), values() — iterators for walking the full set, handy for logging or forwarding a subset of headers somewhere else.
  • forEach(callback) — runs a callback once per header pair, without needing to manually consume an iterator.

What you can't do is mutate it. There's no set() or delete() on the object headers() gives you, because it represents headers that already arrived with the request — by the time your Server Component runs, the request has already happened. If you need to modify outgoing headers (say, adding a custom response header), that's a job for a Route Handler returning a NextResponse, or for proxy.js, not for this function.

// app/api/debug/route.ts
import { headers } from "next/headers";
import { NextResponse } from "next/server";

export async function GET() {
  const headersList = await headers();
  const allHeaders = Object.fromEntries(headersList.entries());

  return NextResponse.json(allHeaders);
}

That example is a genuinely useful debugging trick during development: hit a throwaway route like this and you get a full dump of exactly what your app is receiving, which is often more revealing than reading through proxy or load-balancer documentation.

Dynamic Rendering: The Part That Actually Bites People

This is the detail that causes the most confusion in practice: calling headers() opts your route into dynamic rendering.

The reasoning is straightforward once you see it. Headers are a request-time API — their values can't be known until an actual request arrives, because they depend on the specific client making that specific request. A route that reads them can't be prerendered as static HTML at build time, because there's no way to know in advance what a user-agent or authorization header will contain. Next.js has to render that route fresh, per request, on the server.

That's usually exactly what you want — you're reading headers because you need per-request behavior. But it becomes a problem when a headers() call ends up somewhere you didn't expect, like a shared layout or a utility function imported into an otherwise-static page. Suddenly an entire route tree loses its static optimization because of one incidental call three files deep.

If you're on the newer Cache Components model, this shows up even more explicitly: calling headers() outside of a <Suspense> boundary will prevent that route from being prerendered at all, and Next.js will surface a build error pointing you at the fix options — typically wrapping the header-dependent part of your UI in <Suspense> so the rest of the page can still be served as a static shell.

// app/dashboard/page.tsx
import { Suspense } from "react";
import { headers } from "next/headers";

async function PersonalizedGreeting() {
  const headersList = await headers();
  const userAgent = headersList.get("user-agent") ?? "unknown device";

  return <p>Hello from your {userAgent}</p>;
}

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<p>Loading greeting…</p>}>
        <PersonalizedGreeting />
      </Suspense>
    </div>
  );
}

The rest of DashboardPage — the heading, the surrounding layout — can still be part of the static shell. Only the piece that genuinely needs the request is deferred and streamed in.

A Practical Pattern: Forwarding the Authorization Header

One of the most common real reasons to reach for headers() is proxying an incoming request's credentials to a downstream API, without your Server Component needing to know or care how authentication was set up upstream:

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

export default async function Page() {
  const authorization = (await headers()).get("authorization");
  const res = await fetch("https://api.example.com/me", {
    headers: { authorization },
  });
  const user = await res.json();

  return <h1>{user.name}</h1>;
}

This pattern is especially common when Next.js sits in front of an existing backend that already handles auth via bearer tokens or session headers — you're not reimplementing authentication, just passing along what already arrived.

Common Mistakes

Forgetting to await it. In modern Next.js, headers() returns a promise. Skipping the await either throws or silently gives you a promise object instead of the Headers instance you expected, depending on how strictly your version enforces it. If you see .get is not a function errors, this is almost always the cause.

Trying to set outgoing headers with it. The object is read-only by design. If you need to add a response header, do it in a Route Handler (NextResponse.json(data, { headers: { ... } })) or in proxy.js, not by attempting to mutate what headers() returns.

Calling it somewhere shared and being surprised the whole route went dynamic. Because the dynamic-rendering opt-in applies to the entire request tree that depends on the call, a headers() call buried in a shared layout or a commonly-imported utility can silently remove static optimization from routes that never intended to use it. If a page you expected to be static suddenly isn't, headers() (or cookies(), which behaves the same way) in a shared import is a prime suspect.

Reaching for it when cookies() is really what you want. Cookies technically travel as a cookie header, and you can parse them out of the raw Headers object manually, but Next.js gives you a dedicated cookies() function with a much friendlier API for that specific case — get/set/delete semantics, typed values, and no manual string parsing. Use headers() for headers that aren't cookies.

Key Takeaways

QuestionAnswer
What does it return?A read-only Web Headers object
Is it async?Yes, as of Next.js 15 — always await it
Can I modify outgoing headers with it?No — use a Route Handler or proxy.js instead
Does using it affect rendering?Yes — it opts the route into dynamic rendering
How do I keep the rest of the page static?Wrap the header-dependent UI in <Suspense>
What's the difference from cookies()?cookies() is a dedicated, friendlier API specifically for the cookie header

headers() is a small function, but the "small" is deceptive — the moment you call it, you've made a statement about how that route renders. Treat it as a deliberate opt-in to per-request behavior, isolate it behind <Suspense> when the rest of the page doesn't need to pay that cost, and reach for cookies() instead when cookies specifically are what you're actually after.

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