
Next.js unstable_noStore
Every so often you run into an API in the Next.js docs that's still fully functional, still shipped, still documented — and still flagged as something you probably shouldn't reach for anymore. unstable_noStore is exactly that kind of API. It does one job well (telling Next.js "don't cache the output of this component"), but the framework has since grown a cleaner, more general primitive for expressing the same intent. Understanding unstable_noStore is still worth your time, though, because a lot of code written against Next.js 13 and 14 uses it, and you'll run into it in the wild long before every project finishes migrating off it.
This article covers what unstable_noStore actually does, why it was introduced, why it's now considered legacy, and exactly how to read code that uses it — plus what to reach for instead in a project built on a current version of Next.js.
What unstable_noStore Does
unstable_noStore is a function you call inside a Server Component (or a function that a Server Component calls) to declaratively mark that component as dynamic — meaning its output should not be cached or prerendered at build time. Call it, and Next.js treats everything downstream of that call as something that must be computed fresh on every request.
import { unstable_noStore as noStore } from "next/cache";
export default async function ServerComponent() {
noStore();
const result = await db.query(/* ... */);
return <div>{/* render result */}</div>;
}
Note the import alias in that example — almost every codebase that uses this function renames it to noStore on import, both because unstable_noStore is a mouthful to type repeatedly and because the unstable_ prefix reads oddly next to plain noStore() calls scattered through component bodies. You'll see this alias so consistently in real code that it's worth adopting yourself if you're maintaining a project that still uses this API.
Functionally, calling noStore() is equivalent to fetching with cache: 'no-store', or with next: { revalidate: 0 }. The difference is where you attach that intent. Those fetch options only work if the code doing the data access actually goes through fetch — with noStore(), you get the same "opt this out of static generation" effect regardless of how the data is being read. That matters more than it might sound, because a lot of real-world data access in a Next.js app doesn't go through fetch at all.
Why It Exists: The Problem It Solves
Before unstable_noStore existed, if you wanted to mark part of a route as dynamic, your two options were:
Route segment config: setting export const dynamic = 'force-dynamic' at the top of a page.js or layout.js file. This is a blunt instrument — it applies to the entire route segment, not to one specific component within it. If you had a page with nine components that were all perfectly cacheable and one that read from a database with live inventory counts, force-dynamic would force all ten to skip caching, not just the one that needed it.
Fetch options: passing cache: 'no-store' to individual fetch() calls. This works, but only if your data access is actually implemented as a fetch() call. The moment you're using a database ORM, a gRPC client, a filesystem read, or literally any data-fetching mechanism that isn't the Web fetch API, this option simply doesn't apply — there's no cache configuration object to pass it into.
unstable_noStore closes that gap. It lets you say "this specific component is dynamic" at exactly the granularity of a single component, using a data source of your choosing, without touching the caching behavior of anything else on the same route.
// app/dashboard/page.tsx
import { unstable_noStore as noStore } from "next/cache";
import { StaticHeader } from "./static-header";
import { LiveInventoryCount } from "./live-inventory-count";
export default function DashboardPage() {
return (
<div>
{/* This stays cacheable */}
<StaticHeader />
{/* Only this component opts out of caching */}
<LiveInventoryCount />
</div>
);
}
async function LiveInventoryCountInner() {
noStore();
const count = await inventoryClient.getLiveCount();
return <p>{count} units in stock</p>;
}
This per-component granularity is the entire reason unstable_noStore was introduced in Next.js 14, and it's a genuinely good idea — it's just been superseded by a cleaner implementation of the same idea.
Why It's Now Legacy
As of Next.js 15, the official recommendation flipped: use connection() instead of unstable_noStore. Next.js still ships unstable_noStore for backward compatibility — nothing breaks if you're still calling it — but it will not receive further development, and new projects shouldn't reach for it.
The reasoning behind the switch isn't cosmetic. connection() represents a more accurate mental model of what's actually happening: rather than a special-purpose "don't cache this" flag, connection() returns a promise that only resolves once an actual incoming request connection exists. Since a build-time prerender has no real request to resolve against, awaiting connection() naturally forces the component to wait for an actual request — which has the side effect of marking it dynamic, but does so through a mechanism that composes more naturally with the rest of the request-time API surface (headers(), cookies(), searchParams), all of which share that same "only resolves once a real request exists" behavior.
// The old way
import { unstable_noStore as noStore } from "next/cache";
export default async function LiveInventoryCount() {
noStore();
const count = await inventoryClient.getLiveCount();
return <p>{count} units in stock</p>;
}
// The current way
import { connection } from "next/server";
export default async function LiveInventoryCount() {
await connection();
const count = await inventoryClient.getLiveCount();
return <p>{count} units in stock</p>;
}
Functionally these two produce the same result — a component that opts out of static generation. The difference is conceptual clarity: connection() tells the next developer reading this code "this needs a live request to run," which is the actual constraint, rather than the more indirect "don't store this," which describes the caching consequence of that constraint rather than the constraint itself.
If your project uses Cache Components (the newer explicit-opt-in caching model available from Next.js 15 onward), the picture shifts again: under that model, dynamic behavior is the default and you explicitly mark things as cacheable with 'use cache', rather than explicitly marking things as dynamic. Under Cache Components, you generally won't reach for either unstable_noStore or connection() for this purpose — a component is dynamic simply by not having a 'use cache' boundary around it.
The unstable_cache Interaction Gotcha
There's one behavior worth calling out specifically because it trips people up: calling unstable_noStore() inside a function wrapped with unstable_cache does not opt that function out of caching. The docs are explicit about this, and it's counterintuitive enough that it's worth internalizing rather than discovering by accident.
import { unstable_cache } from "next/cache";
import { unstable_noStore as noStore } from "next/cache";
const getCachedData = unstable_cache(async () => {
noStore(); // This does NOT force this function to skip the cache
const data = await db.query(/* ... */);
return data;
});
Once you're inside an unstable_cache boundary, the cache configuration you passed to unstable_cache itself (its revalidate option, its tags) is what governs whether the result is stored — noStore() called from inside that boundary is effectively ignored. If you actually want a piece of data to never be cached, don't wrap it in unstable_cache in the first place; the two mechanisms aren't meant to be nested against each other's intent.
When You'll Actually Encounter This
If you're starting a brand-new Next.js 16 project, you're unlikely to write a fresh call to unstable_noStore — you'd reach for connection(), or design around Cache Components' 'use cache' boundaries instead. Where this function actually matters day-to-day is:
- Reading an existing codebase written against Next.js 13 or 14, where you'll see
noStore()calls scattered through Server Components and need to know what they mean. - Migrating an older project to a newer Next.js version, where deciding whether to leave
unstable_noStorecalls in place (they still work) or swap them forconnection()(the recommended path) is a real decision you'll face. - Reading third-party library code or examples that predate the
connection()recommendation and haven't been updated.
In all three cases, the mental model is the same: wherever you see noStore(), mentally translate it to "this component needs to run per-request, not be prerendered" — the same thing await connection() expresses, just through an older, more specialized-sounding API.
Common Mistakes
Assuming it's been removed. It hasn't. unstable_noStore is fully supported for backward compatibility in Next.js 16 — "legacy" here means "not the recommended path for new code," not "deprecated to the point of removal." You don't need to rush to replace every instance in a working codebase.
Expecting it to interact with unstable_cache the way fetch's cache: 'no-store' interacts with route-level caching. As covered above, calling it from inside an unstable_cache-wrapped function does not override that function's own cache configuration.
Reaching for force-dynamic instead, out of habit. If you're touching code that already uses unstable_noStore for its per-component granularity, don't "simplify" it by replacing it with route-level force-dynamic — that changes the behavior for the whole route, not just the one component, which is very likely not what the original author intended.
Writing new unstable_noStore calls in a Next.js 15+ project. If you're writing fresh code today, use connection() instead. There's no functional downside to unstable_noStore continuing to work, but there's no upside to adopting it for new work either.
Key Takeaways
| Question | Answer |
|---|---|
| What does it do? | Marks a single Server Component as dynamic, opting it out of caching/prerendering |
| Is it deprecated? | Not removed — legacy since Next.js 15, kept for backward compatibility only |
| What replaced it? | connection(), recommended from Next.js 15 onward |
Does it work inside unstable_cache? | No — calling it there does not override that function's cache config |
| Should I use it in new code? | No — reach for connection(), or design around Cache Components' 'use cache' model instead |
| Import convention | Nearly universally aliased on import: import { unstable_noStore as noStore } from 'next/cache' |
unstable_noStore did its job well for the version of Next.js it was built for: giving developers a way to say "this one component needs to be fresh" without reshaping an entire route around that need. The framework has since found a more conceptually honest way to express that same constraint through connection(), which is why you'll see the recommendation shift in the docs — but you don't need to treat every existing noStore() call in a codebase as broken or urgent to replace. It still works exactly as documented; it's just no longer where new code should start.


