# Why is my deployed project giving a 404?

**Author:** Vercel

---

Fix a Vercel 404 error on a deployment that shows Ready by debugging the routing metadata instead of the filesystem. This guide covers the misconfigurations that produce a 404 on a healthy deployment, the routing model that explains why they happen, and the order to check them in.

The build succeeds, the dashboard says Ready, every file shows up in the Output tab, and every URL still returns 404. When this lands in an incident channel, the next hour usually gets spent in the wrong place.

## Key takeaways

- Vercel routes from metadata generated at build time, not from a live filesystem, so a request can 404 before any file lookup runs.
  
- A Ready status confirms the build succeeded and the deployment exists. It does not confirm that routing works.
  
- The two-URL test is the fastest first triage. If `.vercel.app` works and your custom domain 404s, the deployment is healthy and the problem is DNS.
  
- A Framework Preset set to "Other" lets Vercel skip framework-specific manifest generation, so the files exist with no routing instructions behind them.
  
- Single-page applications need an explicit catch-all rewrite, because Vercel resolves every URL as a filesystem path unless you tell it otherwise.
  

## Route from metadata, not from a filesystem

Most of these cases resolve before you open a single config file, once you correct one assumption. On a traditional server, debugging a 404 starts with checking whether the file exists on disk. That instinct is correct there and wrong here. Vercel does not serve from a live filesystem. It routes from metadata generated at build time and uploaded to static storage, and the routing layer can reject a request before it ever looks for a file.

Follow the path a request takes. It lands at the nearest of Vercel's 126 Points of Presence (PoPs), which terminates TCP and forwards it to the nearest region over a private network in single-digit milliseconds. The region terminates TLS, then runs the request through a fixed sequence of routing stages:

1. Firewall
   
2. Microfrontends
   
3. Skew Protection
   
4. Rolling Releases
   
5. Bulk Redirects
   
6. Project Routes
   
7. Deployment Routes
   
8. Headers and Redirects
   
9. Routing Middleware
   
10. File System Routes
    
11. Rewrites
    

The deployment's metadata decides how each request moves through that pipeline. When the path matches no route in the metadata, the CDN returns 404 without ever checking whether a file exists in the build output. That single fact explains how a file can sit in the Output tab and still produce a valid 404: the routing layer never reaches the filesystem lookup, because nothing in the metadata told it to.

So stop debugging the filesystem and start debugging the metadata. There is no disk to check permissions on and no server process to restart. The question is whether the routing metadata matches the request shape the CDN expects. Almost every fix below corrects an input that generated the wrong metadata.

## Match the symptom to its cause before you touch config

A 404 on a healthy deployment has several distinct causes that share a symptom, and editing config before you know which one you have wastes time. Match what you observe in the left column, then go to the section that covers the cause.

| What you see                                          | Most likely cause                                                           | First thing to check                   |
| ----------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------- |
| Sitewide 404 on a build that succeeded                | Framework Preset set to "Other," so no routing manifest was generated       | Project Settings, Framework Preset     |
| `.vercel.app` works, custom domain 404s               | DNS or domain configuration                                                 | The two-URL test, then DNS records     |
| Dynamic route 404s, static routes return 200          | Route not configured for dynamic rendering, or empty `generateStaticParams` | Route handler exports                  |
| SPA client route 404s on refresh or direct navigation | No catch-all rewrite for client-side routing                                | `vercel.json` rewrites                 |
| File present in the Output tab, still 404s            | Routing metadata has no matching route                                      | Routing manifest, not the filesystem   |
| Previously working deployment starts 404ing           | Regional infrastructure issue or a stale manifest from a failed deploy      | Deployment history and incident status |

Starting here cuts the search space before you change anything. Each row maps to a section below, and most of them resolve at the first check.

## Start with the two-URL test

The instinct carried over from traditional servers is to confirm the file exists. On Vercel that is the wrong first move, because the routing layer can reject a request long before any file lookup, which means the file can be present and the 404 can still be correct. Start with the two-URL test instead.

Check whether the `.vercel.app` URL serves your content. If it does and the custom domain returns 404, the deployment is healthy and the problem is domain configuration or DNS propagation. If both URLs return 404, the problem is in the build output or the routing manifest. One test rules out either the entire DNS surface or the entire build surface, so run it before anything else.

## Check the framework preset and output directory

Two settings in this category produce a sitewide 404 on a build that completed without errors, and both are metadata problems that look like filesystem problems: the files are fine, the instructions for reaching them are not.

### Framework Preset set to "Other"

When the whole site 404s, look at the Framework Preset in Project Settings. If the preset is set to "Other" for a project that should be detected as a specific framework, Vercel can skip framework-specific routing manifest generation. The build completes, the files exist, and the CDN has no routing instructions for the pages, so every request returns 404. This shows up in community reports where a Next.js project deploys without errors and every route 404s, and the fix is a single dropdown change:

1. Open Project Settings, then Build and Deployment.
   
2. Go to Framework Settings.
   
3. Select the correct framework, for example Next.js.
   
4. Redeploy.
   

The reason the deployment can work locally while failing on Vercel is that local development never exercises the routing metadata. `next dev` and `next start` route from the filesystem. The CDN routes from a manifest generated during the build, and that manifest is only generated correctly when the platform knows which framework it is building. Local success tells you nothing about whether the manifest exists.

### Incorrect Output Directory

When the Output Directory setting does not match where the build tool drops files, Vercel serves an empty folder and every path 404s. For Next.js projects, the output directory is configured through the `distDir` property in `next.config.js`. For other frameworks, confirm in Project Settings that the directory matches the actual build output.

## Fix Next.js dynamic routes that 404 in production

After sitewide 404s, dynamic routes are the most common report, and they fail for more than one reason. The shared mechanism is that the production build either does not include the requested params or was never told to render the route dynamically. Local rendering hides all of it, because `next dev` resolves these paths on demand, which is why "it works locally" carries no information here. Check these variants in order.

### Dynamic API routes that 404 despite appearing in the Functions list

This hits App Router projects where a static route like `/api/health` returns 200 but a dynamic route like `/api/quiz/[sessionId]` returns 404, and the response headers show `X-Matched-Path: /404` with no function execution logs. The fix is explicit exports on the route handler:

`export const runtime = 'nodejs' export const dynamic = 'force-dynamic' export const dynamicParams = true`

The tradeoff: `force-dynamic` opts the route out of static optimization, so you trade Vercel's cached, prerendered response for a function invocation on every request. Use it where the route genuinely needs per-request data, not as a blanket fix to make a 404 disappear.

### Empty `generateStaticParams` with no fallback

When `generateStaticParams` returns an empty array and `dynamicParams` is not set to `true`, every non-prerendered route 404s. The correct incremental static regeneration (ISR) pattern combines `dynamicParams = true` with a `revalidate` interval and a `notFound()` call for content that genuinely does not exist:

`import { notFound } from 'next/navigation' export const dynamicParams = true export const revalidate = 60 export async function generateStaticParams() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()) return posts.map(post => ({ slug: post.slug })) } export default async function Page({ params }: { params: { slug: string } }) { const post = await getPost(params.slug) if (!post) { notFound() } return <div>{post.title}</div> }`

### Parallel routes missing a `default.js`

Parallel routes render a `default.js` file for unmatched slots, and when that file does not exist for a slot, Next.js renders a 404 instead. On refresh or direct navigation it cannot recover the active state and falls back to `default.js`, so create one for every slot even if it returns null.

### Middleware bundling failures

Middleware bundling failures cascade further than people expect. When `next/server` pulls a Node.js-incompatible dependency like `ua-parser-js` into the middleware bundle, the resulting `__dirname is not defined` error does not stay scoped to middleware. Routing Middleware runs as part of the routing pipeline itself, so a middleware build failure can break all routing at once.

### A `vercel.json` and `next.config.js` routing conflict

With Next.js on Vercel, routing belongs in `next.config.js` and should replace `vercel.json` routing entirely. Configure rewrites in `next.config.js` and remove them from `vercel.json`. Using both creates conflicts where the `vercel.json` rules override or interfere with the framework's own routing manifest.

## Add an SPA fallback so client routes resolve

Vercel resolves every URL as a server-side filesystem path by default. That is correct for frameworks built on filesystem routing, like Next.js or SvelteKit. It is wrong for single-page applications (SPAs) built with Vite, Create React App, or similar tools, which load `index.html` once and then let the browser handle routing. Vercel cannot infer that a project is an SPA, because most frameworks rely on filesystem routing, so you have to make the handshake explicit with a catch-all rewrite in `vercel.json`:

`{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }`

When `cleanUrls: true` is set, the destination has to drop the `.html` extension:

`{ "rewrites": [{ "source": "/(.*)", "destination": "/index" }], "cleanUrls": true }`

If the SPA also serves API routes, exclude them from the fallback with a negative lookahead, or every API call gets rewritten to `index.html` too:

`{ "rewrites": [{ "source": "/((?!api/).*)", "destination": "/index.html" }] }`

Framework-specific behavior matters here, and copying the wrong pattern reintroduces the 404. The catch-all rewrite is for SPAs built with tools like Vite or Create React App, not for frameworks with their own routing:

- **SvelteKit** projects should not use `vercel.json` rewrites for SPA fallback, because the rewrites apply only at the Vercel proxy layer and SvelteKit does not see the rewritten URL at runtime. Use the framework-native behavior in the SvelteKit guide instead.
  
- **Nuxt** projects configure the `routeRules` property in `nuxt.config.js` or `nuxt.config.ts`, documented in the Nuxt docs.
  
- **Astro** projects are static by default and deploy without extra config, but server-side rendering needs the Vercel adapter.
  

Two traps catch people repeatedly. First, `vercel.json` has to live at the repository root, be committed to Git, and not be in `.gitignore`. When the file is not committed, the rewrites never apply, and Vercel gives you no error, so verify it is tracked with `git log`. Second, a Framework Preset set to "Other" can stop even a correctly written `vercel.json` rewrite from taking effect, which is the same preset problem from the framework section showing up again here.

## Point your custom domain at Vercel correctly

When `.vercel.app` serves content and the custom domain 404s, stop looking at the build entirely. The deployment is healthy and the problem sits between your DNS provider and Vercel's network. The record you need depends on the domain type:

| Domain type | Record       | Example            |
| ----------- | ------------ | ------------------ |
| Apex domain | A record     | `example.com`      |
| Subdomain   | CNAME record | `docs.example.com` |

Both have to match exactly what Vercel shows in Project Settings under the Domains page. CNAME records have to include the trailing period that marks a fully qualified domain name, and outdated records from a previous host create conflicts. The domain setup and DNS troubleshooting docs cover the expected record types.

Two commands settle most of it:

`dig A api.example.com +short @ns1.vercel-dns.com vercel domains inspect example.com`

The first shows what DNS currently resolves to. The second shows the exact records Vercel expects. When the two disagree, you have found the problem.

Propagation produces a subtler version of the same failure. Before pointing DNS records at Vercel, lower the time to live (TTL) to 60 seconds and wait for the old TTL to expire, then change the records, verify they resolve, and raise the TTL back to a normal value. The cost of a low TTL is more frequent resolver lookups, which is why you lower it only for the cutover. Skip the step and propagation lag produces intermittent 404s that are hard to reproduce, because half your requests resolve to the old host and half to the new one.

SSL certificate generation can also look like a 404. For non-wildcard domains, Vercel uses Let's Encrypt's HTTP-01 challenge and handles it automatically as long as the domain points to Vercel, and Let's Encrypt needs to reach `/.well-known/acme-challenge/` to issue the certificate. Three things commonly block that path and stop the certificate from issuing:

- A custom firewall in front of the domain
  
- A Web Application Firewall (WAF) rule that filters the request
  
- A stale `_acme-challenge` TXT record left over from a previous setup
  

Wildcard domains use the DNS-01 challenge instead, which requires nameservers pointed at Vercel. The SSL troubleshooting guide covers these blockers.

One more edge case: a domain can only belong to one Vercel account at a time. When it is linked elsewhere, the dashboard shows "The domain is not available." Use the "Add Existing" option on the Domains dashboard and add a TXT record to verify ownership, and the domain transfers once verified.

## Read the routing evidence Vercel exposes

The evidence lives in the dashboard, not behind SSH and log files. These tools map directly onto the metadata model, and used in order, they tell you which half of the problem you are in without guesswork.

The Output tab on a deployment's details page shows exactly which files the build produced. When expected routes are not listed there, the problem is build configuration, not routing. This is the fastest way to separate "the build did not produce the file" from "the build produced the file, but routing did not match it." For projects on the "Other" preset, Vercel prioritizes files in the `public` folder over those in the root directory.

Runtime Logs filter by HTTP status code, environment, and Git branch. Filter for 404s on a specific route, and watch for the function invocation. When no invocation log appears for a dynamic route that 404s, the route handler is never being called, which points at a routing failure rather than a handler failure. That distinction tells you where to look next, and Runtime Logs expose it directly. Retention varies by plan, so capture the evidence inside that window:

| Plan                      | Log retention |
| ------------------------- | ------------- |
| Hobby                     | 1 hour        |
| Pro                       | 1 day         |
| Enterprise                | 3 days        |
| Observability Plus add-on | 30 days       |

For agent-driven debugging, Vercel's MCP (Model Context Protocol) server exposes runtime logs to AI coding agents through the `get_runtime_logs` tool. An agent can retrieve logs for a project or deployment, inspect function output, and investigate runtime behavior without manual dashboard navigation, which turns the same routing evidence you would read by hand into context the agent can query. When you need longer retention or want the data in your own stack, Drains stream logs and traces to an external endpoint.

With those tools in hand, the diagnostic order that resolves most cases is:

1. Run the two-URL test. If `.vercel.app` works and the custom domain does not, the problem is domain or DNS. If both fail, the problem is in the build output or routing manifest.
   
2. Check the Output tab for the expected files.
   
3. Check Runtime Logs filtered for 404s, and look for missing function invocations on dynamic routes.
   
4. Check for uncaught errors in functions or components, which can bubble up into 404s, and wrap critical paths in `try/catch`.
   
5. Verify the Framework Preset matches the actual framework.
   

Look across the whole set of causes, and they share a root. The build succeeds because the files exist. The 404 happens because the routing manifest was generated from the wrong inputs: the wrong framework preset, the wrong output directory, a missing SPA fallback, or a stale manifest carried over from a failed deploy. The surface differs every time, and the root cause does not. The Ready badge was never a routing guarantee. Debug the metadata, not the filesystem, and the order above will get you to the cause faster than checking whether the file is there.

## FAQs about Vercel 404 errors

### Why does my Next.js app work locally but return 404 on Vercel?

Dynamic routes can 404 after deployment when the production build does not include the requested params or when a route handler is not configured for dynamic rendering. Dynamic routes need explicit configuration: either `generateStaticParams` with `export const dynamicParams = true`, or `export const dynamic = 'force-dynamic'` on the route handler. Also confirm the Framework Preset is set to Next.js, not "Other."

### My custom domain shows 404 but my `.vercel.app` URL works. What's wrong?

The deployment is healthy and the problem is DNS configuration. Verify the A record for apex domains or the CNAME record for subdomains matches the values in Vercel's Project Settings. Check for outdated DNS records from a previous provider, and confirm SSL certificate generation succeeded by making sure nothing blocks `/.well-known/acme-challenge/`.

### Why do I get 404 on client-side routes in my React or Vite SPA?

Vercel resolves every URL as a server-side path by default, so SPAs need a catch-all rewrite in `vercel.json`: `{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }`. The file has to sit at the repository root and be committed to Git, or the rewrite never applies and Vercel gives you no error.

### Can a previously working deployment start returning 404s?

Yes. Regional infrastructure issues can cause deployment failures independent of project configuration. Vercel documented one such disruption on October 20, 2025, when AWS API issues in us-east-1 caused new deployments using Routing Middleware or functions with iad1 as the selected region to fail. Protected Source Maps, enabled by default on new projects, also return 404 to non-authenticated requests for `.map` files.

---

[View full KB sitemap](/kb/sitemap.md)
