
Nextjs draftMode function
If you've ever built a site backed by a headless CMS, you've hit this problem: an editor saves a draft, previews it, and it doesn't show up because your page is served from a cache built at the last deploy. draftMode is the function that gets you out of that bind. It's a small API — three properties, really — but it sits at the center of how Next.js lets you punch a temporary hole through your caching layer for exactly one browser session, without touching what everyone else sees.
This article is the API reference for the function itself: what it returns, where you're allowed to call it, and the mechanics of the cookie it sets. If you want the end-to-end "how do I wire this up to my CMS" walkthrough, that's a separate guide on this blog. Here we're going deep on draftMode() as a piece of the API surface.
What draftMode Actually Is
draftMode is an async function you import from next/headers. Calling it gives you an object with one property and two methods:
import { draftMode } from "next/headers";
export default async function Page() {
const { isEnabled } = await draftMode();
}
| Member | What it does |
|---|---|
isEnabled | A boolean telling you whether Draft Mode is currently active for this request. |
enable() | Turns Draft Mode on by setting a cookie named __prerender_bypass. |
disable() | Turns Draft Mode off by deleting that cookie. |
That's the entire public API. Everything else is about where you're allowed to call these three things and what happens when you do.
Why It's Async
Since Next.js 15, draftMode() returns a Promise, so you need await (or React's use() if you're consuming it from a place that supports it):
const draft = await draftMode();
If you're coming from Next.js 14 or earlier, draftMode() used to be synchronous. Next.js kept a backwards-compatible synchronous access path for a while, but it's flagged as deprecated, and it will not survive indefinitely. If you're maintaining an older codebase, this is one of the things the version-15 codemod (upgrade/codemods#150) rewrites for you automatically — worth running rather than hand-fixing every call site.
The reason it became async in the first place is the same reason cookies() and headers() did: Next.js wants request-scoped APIs to be awaited so it can correctly track which parts of your render actually depend on request data, which matters a lot once you're using 'use cache' and Cache Components (more on that below).
The Bypass Cookie, and Why It's Safe
enable() works by setting a cookie called __prerender_bypass. That cookie's value isn't your session ID or anything predictable — Next.js generates a fresh, random bypass value every time you run next build. That's a deliberate security property: someone can't guess last month's value and reuse it against this month's deployment, because the value doesn't exist anymore after a rebuild.
This has a practical implication that catches people off guard: if you enable Draft Mode, then redeploy, every previously-issued draft session is invalidated. Editors who had drafts open across a deploy will suddenly see production content again and need to re-enter draft mode. That's expected behavior, not a bug — just something worth telling your content team about before it surprises them mid-review.
Calling enable() and disable() — Only From a Route Handler
You can't call enable() or disable() from a Server Component, because setting a cookie requires access to the outgoing response, and Server Components don't have one during a normal render. Both methods are meant to be called from a Route Handler:
// app/draft/route.ts
import { draftMode } from "next/headers";
export async function GET(request: Request) {
const draft = await draftMode();
draft.enable();
return new Response("Draft mode is enabled");
}
Disabling mirrors it exactly:
// app/draft/route.ts
import { draftMode } from "next/headers";
export async function GET(request: Request) {
const draft = await draftMode();
draft.disable();
return new Response("Draft mode is disabled");
}
A common real-world pattern is a single /api/draft route that branches on a query parameter (?disable=true) rather than maintaining two separate routes — fewer moving pieces for your CMS integration to configure.
The <Link prefetch={false}> Gotcha
This is the single most common mistake people make with Draft Mode, and it's easy to miss because it fails silently. If you link to your disable route with a plain <Link>, Next.js will prefetch it in the background — which means it actually invokes the Route Handler and deletes your cookie before the user even clicks:
// Wrong: prefetching this link silently exits Draft Mode
<Link href="/draft?disable=true">Exit draft mode</Link>
// Right
<Link href="/draft?disable=true" prefetch={false}>Exit draft mode</Link>
The symptom is maddening to debug if you don't know to look for it: an editor reports that draft mode "randomly turns itself off" while they're just browsing around the site, nowhere near the exit link. If you see that report, check every link pointing at your enable/disable routes for a missing prefetch={false} before you go looking anywhere else.
Checking isEnabled in a Server Component
Reading the current state is the one thing you can do outside a Route Handler — it's just a read, not a cookie mutation:
// app/page.tsx
import { draftMode } from "next/headers";
export default async function Page() {
const { isEnabled } = await draftMode();
return (
<main>
<h1>My Blog Post</h1>
<p>Draft Mode is currently {isEnabled ? "Enabled" : "Disabled"}</p>
</main>
);
}
A realistic version of this fetches unpublished content only when isEnabled is true, and falls back to your normal published-content query otherwise:
async function getPost(slug: string) {
const { isEnabled } = await draftMode();
const status = isEnabled ? "draft" : "published";
return cms.getPost(slug, { status });
}
Draft Mode and Caching Directives
This is the part of the reference that trips people up once they've adopted 'use cache' and Cache Components. isEnabled is readable inside a caching directive's scope — that part is fine, and it's actually essential, since it's how a cached component can decide to skip the cache. But cookies() and headers() are not allowed inside a caching directive scope, even while Draft Mode is active, and calling enable() or disable() from inside one will throw outright.
The reasoning follows directly from what a caching directive is for: a 'use cache' function is meant to produce output that can be reused across requests and users. cookies() and headers() are inherently request-specific, so allowing them there would silently break the caching contract. draftMode().isEnabled gets a narrow exception because Next.js needs a way for cached code to detect draft state and bail out of the cache correctly — see the next section for what "bail out" means in practice.
The practical upshot: when Draft Mode is enabled, everything under a caching directive's scope re-executes on every request, and nothing gets written to the cache. That's exactly the behavior you want — draft content should never accidentally get baked into a shared cache entry that a regular visitor might see — but it does mean you shouldn't be surprised by a temporary performance dip while an editor is actively reviewing drafts. It's the cache correctly refusing to do its job for that one session, not a bug.
Testing It Locally
If you're testing Draft Mode over plain HTTP on localhost, know that the bypass cookie is a genuine cookie, and your browser needs to actually accept third-party cookies and allow local storage for the flow to work end to end. If you've got aggressive privacy settings or a browser extension blocking cross-site cookies in development, you can end up debugging "Draft Mode won't turn on" for twenty minutes before realizing it's your browser configuration, not your code. Testing in a plain, unrestricted browser profile first is the fastest way to rule that out.
Comparing This to the Pages Router
If you're coming from the Pages Router, this maps directly onto what used to be called Preview Mode (setPreviewData / res.clearPreviewData / context.preview). The concept is identical — a signed cookie that flags a request as "wants unpublished content" — but the API surface is cleaner in the App Router: one function, one object, three members, instead of juggling separate imperative calls scattered across getServerSideProps and API routes.
Common Mistakes
Forgetting prefetch={false} on the exit link. Covered above, but worth repeating because it's the single most reported "bug" that isn't actually a bug.
Calling enable()/disable() from a Server Component. You'll get a runtime error, because there's no response object to attach the cookie to outside a Route Handler.
Assuming the bypass cookie survives a redeploy. It doesn't, by design. Don't build a workflow that depends on long-lived draft sessions across deploys without warning your editors.
Reaching for cookies() inside a 'use cache' function to check draft state manually. Use draftMode().isEnabled instead — it's the one request-scoped read explicitly allowed inside a caching directive's scope, and it composes correctly with the cache-bypass behavior described above.
Key Takeaways
| Question | Answer |
|---|---|
What does draftMode() return? | { isEnabled, enable(), disable() } |
Where can I call enable()/disable()? | Only inside a Route Handler |
Where can I read isEnabled? | Server Components, and inside caching directive scopes |
| What sets/clears the cookie? | __prerender_bypass, regenerated on every next build |
| Does a draft session survive a redeploy? | No — the bypass value changes on every build |
Can I call cookies()/headers() inside 'use cache' during Draft Mode? | No — only draftMode().isEnabled is allowed there |
| Most common bug? | Forgetting prefetch={false} on the link that disables Draft Mode |
draftMode is a small API doing an outsized job: it's the one sanctioned escape hatch from your caching layer, scoped tightly enough that it can't leak into anyone else's request. Once you internalize the Route-Handler-only write restriction and the caching-directive exception for isEnabled, the rest of it — enabling, disabling, checking state — is about as simple as Next.js APIs get.


