Vercel 404 NOT_FOUND After Deployment: Causes and Fixes
The build log is green, the deployment says Ready, and the page says 404: NOT_FOUND. Searching for it mostly returns forum threads where each person had a different cause, which is the real problem: "404 after deploy" is a symptom with at least four unrelated sources. This post sorts them in the order I would check them.
Everything about Vercel below comes from its documentation as read in November 2026. The Next.js parts come from the Next.js 16 docs. I'm not describing incidents of my own here; this is a checklist built from the docs.
What the error actually says
Vercel documents NOT_FOUND as the error that occurs when a requested resource could not be found, for example because it was moved, deleted, or the URL has a typo. It has a close relative, DEPLOYMENT_NOT_FOUND, for a request to a deployment that doesn't exist (wrong deployment URL or ID, or the deployment was deleted).
The documented troubleshooting steps are short:
- Check the deployment URL for typos or wrong paths.
- Check that the deployment exists and wasn't deleted.
- If it exists, read the deployment logs.
- Check you have permission to access it.
That is the right first minute. The rest of this post is for when all four pass and you still get a 404 on a deployment that built successfully.
Step 1: is the 404 from the platform or from your app?
Open the exact URL in the browser and look at the page.
- Vercel's own error page with the
NOT_FOUNDcode: the platform couldn't find anything to serve for that path. Your app never ran. Go to steps 2 and 3. - Your own 404 page: the platform routed the request to your app, and your app decided the route doesn't exist. Go to step 5.
Which one it is decides everything that follows, and most threads skip this question.
Step 2: output directory and framework preset
Vercel's build docs say that only the contents of the Output Directory are served statically. If Vercel detects a framework, the output directory is configured automatically. If it doesn't detect one, the Framework Preset becomes "Other", and for "Other" the docs say the output directory is public if that folder exists, otherwise . (the project root).
That gives you two common ways to get a 404 on a successful build:
- The preset is wrong or "Other" on a project that needs a framework. A build that writes to
distorbuild, served with an output directory pointing somewhere else, has nothing at/index.htmlto find. Open Settings, then Build and Deployment, and compare the Framework Preset and Output Directory against where your build command really writes. - A stale override. The docs note that a changed Override setting applies from the next deployment. If you fixed the setting and tested the old deployment, you are looking at the old behavior. Redeploy.
You can also pin these per deployment in vercel.json with the framework and outputDirectory fields, which override the project setting for that deployment. If a vercel.json in the repository disagrees with the dashboard, the file is the one to suspect.
Root Directory in a monorepo
If the app lives in a subfolder, the project's Root Directory has to point at it. A Root Directory set to the repository top level, when the actual app is in a subfolder, builds something other than what you meant. The docs also describe a switch, "Include files outside the root directory in the Build Step", on by default, which lets the build reach files outside that directory. Turning it off means the build can't.
Step 3: static site, refresh on a route returns 404
This is the classic one for single page apps: the home page loads, you click to /about, it works, you refresh, and you get NOT_FOUND. In a client-side-routed SPA there is only one real file, index.html; /about exists only inside the JavaScript router.
Vercel's vercel.json docs give the fallback rewrite for this exact case ("often used for a Single Page Application"):
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}Two details from the same page matter:
- The docs say that precedence is given to the filesystem before rewrites are applied, so real files such as your JavaScript bundles are still served as files, and the rewrite only catches paths that don't match one.
- Redirects are processed before rewrites. If you also have a
redirectsrule that matches the same path, it wins.
Note this fix is for a static SPA. A framework with its own server-side routing, like Next.js, shouldn't need it.
Step 4: trailing slash and path differences
If /docs works and /docs/ doesn't, or the other way around, check trailingSlash in vercel.json. The docs say that with trailingSlash: false, a path ending in a slash gets a 308 redirect to the version without it. That's a redirect, not a 404, so a real 404 on one form points to a missing file or route rather than the slash setting.
Step 5: your Next.js app returned the 404
If the 404 page is yours, the platform did its job and the app said no. In the App Router there are a few documented ways that happens:
notFound()was thrown. CallingnotFound()in a route segment renders the nearestnot-foundfile. The standard pattern is a data lookup that returns nothing for the slug you asked for. If production data differs from your local data (an empty database, a missing environment variable, a failed fetch treated as "no result"), every dynamic page 404s even though the build passed. Log what the lookup returns before you blame routing.- The route doesn't match any file. An unmatched URL gets the app's 404. Check the actual folder names under
app, including case, since a mismatch that works on a case-insensitive laptop filesystem won't on a Linux build. dynamicParams = false. The default istrue, meaning a dynamic segment not produced bygenerateStaticParamsis rendered on demand. If you set it tofalse, only the pre-generated params exist and every other value 404s. This option is not available when Cache Components is enabled.
One more Next.js detail that confuses debugging: when a not-found page is streamed, the HTTP status is 200, not 404, and Next.js adds a noindex robots meta tag to the HTML instead. The docs explain that the headers were already sent by the time notFound ran. So "my page shows Not Found but the status code is 200" is documented behavior, not a Vercel bug. If you need a real 404 status, the docs say to verify the resource exists before the response body streams, for example with a check in proxy.
For the global case, an unmatched URL across the whole app, Next.js 16 has an experimental global-not-found.js behind experimental.globalNotFound. The docs say it bypasses normal rendering, so you have to import your own global styles and fonts into it. I would leave it alone unless your app has multiple root layouts or a root layout built on a dynamic segment, which are the two cases the docs name for it.
Step 6: read the logs, then check what the domain points at
If the platform error page shows up and the config looks right, read the deployment's build logs for what the build actually emitted and compare it with the Output Directory you configured.
Also keep in mind that the deployment URL and your production domain are different things. The deployment you just built can be fine while the domain you're visiting is serving an older deployment, or a deployment that was removed. Test the generated deployment URL directly first; if it works and the domain doesn't, the problem is in the domain assignment, not your build. I haven't covered domain setup in this post.
A quick order of checks
- Is the URL right, and does the deployment exist?
- Platform error page, or your own 404 page?
- Framework Preset, Output Directory, Root Directory: do they match what the build produces? Redeploy after changing them.
- Static SPA: add the fallback rewrite.
- Next.js: log the lookup behind
notFound(), check folder names anddynamicParams. - Open the generated deployment URL directly to separate a build problem from a domain problem.
Summary
A Vercel 404 on a green build is almost never one problem. The most useful single question is who produced the 404: Vercel's platform page means nothing was found to serve (check preset, output directory, root directory, SPA rewrite), while your own page means your app ran and chose to say no (check notFound() and the route files). Settings changes only apply on the next deployment, so redeploy before concluding a fix didn't work.