Next.js 404 With fallback: false? Why It Happens and the Fix
Twelve new tools got added to the database in one afternoon. Twelve new pages should have gone live alongside them. Instead, visiting `/tools/grok` returned a 404 — not a slow load, not a broken layout, a clean, confiden…
Twelve new tools got added to the database in one afternoon. Twelve new pages should have gone live alongside them. Instead, visiting /tools/grok returned a 404 — not a slow load, not a broken layout, a clean, confident "this page doesn't exist," for a tool that very much existed in the database.
What Next.js Was Actually Doing
The tool page uses static generation — getStaticPaths and getStaticProps — which means Next.js builds the HTML for each tool page ahead of time, at deploy time, rather than generating it fresh on every request. That's a deliberate performance choice: pre-built pages serve instantly, with no database query standing between a visitor and the page loading.
getStaticPaths is the function responsible for telling Next.js which pages to pre-build. It returned a list of slugs, and it had one more setting attached to it:
export async function getStaticPaths() {
return {
paths: tools.map(t => ({ params: { slug: t.slug } })),
fallback: false,
};
}
fallback: false tells Next.js something specific and easy to miss: *any path not in this list is a 404, permanently, until the next full rebuild.* Not "slow to load" — not found, full stop. It's not a bug in the traditional sense. It's the framework doing exactly what it was configured to do.
Where the List Came From
The paths array was built from tools.map(...) — but tools here was the original static, hardcoded array bundled into the JavaScript at build time, not a live query against the database. The twelve new tools existed in Supabase. They did not exist in the array Next.js was reading when it decided which paths to pre-build. As far as getStaticPaths was concerned, /tools/grok simply wasn't a page that existed — because the only source of truth it consulted was already stale the moment those twelve rows were inserted.
This is the quiet failure mode of static generation paired with a database that changes after the last build: the site doesn't know what it doesn't know. Adding a row to Supabase doesn't retroactively add a path to a list that was already frozen into the JavaScript bundle at build time.
The Fix Has Two Parts
Fixing the missing pages required addressing both halves of the problem — the frozen path list, and the false setting telling Next.js to treat anything outside it as permanently missing.
Part one: read the path list from Supabase instead of the static array, so newly added tools are visible to getStaticPaths without needing a full rebuild first:
export async function getStaticPaths() {
const { data } = await listTools({ status: 'published' });
const liveTools = data?.length > 0 ? data : staticTools;
return {
paths: liveTools.map(t => ({ params: { slug: t.slug } })),
fallback: 'blocking',
};
}
Part two — the one that actually fixes the 404, not just the path list — is fallback: 'blocking'. This setting changes what happens when someone requests a path that wasn't in the pre-built list: instead of returning a 404 immediately, Next.js generates that page on the fly, on the first request, using the same getStaticProps logic it would have used at build time — then caches the result so every subsequent visitor gets the fast, pre-built version. The first visitor to a brand-new tool page waits slightly longer than normal. Every visitor after that gets the same instant static load as everything else on the site.
getStaticProps needed the matching update — falling back to a live Supabase lookup by slug when the tool isn't found in the static array, so the page actually has data to render once fallback: 'blocking' triggers the on-demand build:
export async function getStaticProps({ params }) {
let tool = staticTools.find(t => t.slug === params.slug);
if (!tool) {
const { data } = await getToolBySlug(params.slug);
tool = data;
}
if (!tool) return { notFound: true };
return { props: { tool }, revalidate: 3600 };
}
Why fallback: false Isn't Wrong, Just Wrong Here
fallback: false is the correct setting when the full list of valid paths is genuinely fixed and known ahead of time — a fixed set of legal pages, a small marketing site with a handful of routes that never change without a deploy. In that context, treating anything outside the list as a real 404 is exactly right; there's no legitimate page it could be.
The mismatch here was pairing that assumption with a data source — a growing tools directory — that's specifically designed to change between deploys. The setting wasn't a mistake in isolation. It was the wrong setting for a list of paths that was never actually fixed.
Verifying the Fix
Before this shipped, the specific failing case — /tools/grok, one of the twelve tools added that afternoon — was checked directly against the live site after deploy, not just assumed fixed because the code looked right. It returned the actual tool page, fully rendered, with data pulled from Supabase. The other eleven were spot-checked the same way rather than assumed to work identically just because they shared the same code path.
Takeaway: fallback: false is a promise that your list of valid paths is complete and won't change without a rebuild. The moment that promise stops being true — because the underlying data can grow between deploys — every path outside the last-known list becomes a silent, confident 404, and nothing in the build process will warn you about it.
📬 Get the latest AI tool reviews
Expert picks and comparisons, weekly. No spam.