Vercel Deployment 404 After Successful Build: Diagnosis & Fixes
Vercel shows Ready but your app returns 404. Root cause: routing metadata, not code. Fix framework preset, output directory, SPA fallback, DNS. Step-by-step troubleshooting.
- Your Vercel deployment shows Ready but returns 404 because routing metadata is broken, not your code
- Framework preset set to 'Other' when it should match your stack (Next.js, React, Vite) is the single most common cause
- The Two-URL Test (check.vercel.app vs. custom domain) diagnoses whether the issue is DNS, config, or routing in 30 seconds
- React and Vite SPAs need a vercel.json rewrite rule; Next.js dynamic routes need generateStaticParams() or force-dynamic
- Stop troubleshooting Vercel configuration entirely by switching to managed hosting like Ship
You deployed to Vercel. The build log shows "Deployment Ready" in green. You visit the URL and get 404. This is infuriating because it works on localhost.
The 404 is not in your code. It is in Vercel's routing metadata. Vercel tried to serve a file that does not exist in the output directory. The build succeeded because the build itself succeeded, not because Vercel knows how to route requests correctly. This guide walks you through the diagnosis and fix.
Why Vercel Returns 404 Despite "Ready" Status
Vercel's build succeeds when your code compiles and the output folder is created. It does not succeed or fail based on whether Vercel can serve your routes correctly.
Vercel routes requests based on the structure of your .next/, dist/, build/, or public/ folder plus routing metadata. The metadata comes from your framework preset, vercel.json config, and environment variables. If Vercel sees a request for /about but only has /index.html in the output directory, and no rewrite rule says "send /about to /index.html", Vercel returns 404.
This is why the build log lies. It succeeded. The routing did not. The build step and the routing step are independent. Think of it this way: Your build script produces files. Vercel then looks at those files and tries to figure out how to serve them. If the files are there but Vercel does not understand the structure, or if Vercel has no instructions for certain routes, it returns 404.
Lovable projects hit this constantly. Lovable generates a React SPA but does not generate a vercel.json with the SPA fallback. You ship to Vercel, get 404 on /about, /contact, any non-root path. Your app works locally because localhost does not care about routing metadata; it just serves files.
The good news: all five root causes have one-sentence fixes. Diagnosis takes 30 seconds.
Diagnose Fast: The Two-URL Test
Do this first. It takes 30 seconds and tells you which fix to apply.
- Visit your
.vercel.appURL (the one Vercel assigns automatically). Try a few different routes: the root, a nested path, a dynamic route. - Visit your custom domain (if you have one). Try the same routes.
What you see tells you everything:
- .vercel.app works, custom domain 404s: DNS issue. Your custom domain is not pointed at Vercel's servers. Skip to Fix #5.
- Both 404: Output directory, framework preset, or SPA fallback issue. Continue to Fix #1.
- .vercel.app 404s on some routes, works on others: Dynamic routes or SPA fallback. Continue to Fix #3 (SPA) or Fix #4 (Next.js dynamic routes).
- Everything 404s, even /: Build output is empty or broken. Check Runtime Logs in the Deployment tab. Look for "no output directory found" or "build failed".
This test isolates whether the problem is infrastructure (DNS), configuration (preset, directory), or routing rules (vercel.json, dynamic params).
Fix #1: Check Your Framework Preset (Sitewide 404)
Vercel needs to know whether you shipped Next.js, React, Vite, static HTML, or something else. If Vercel does not know, it defaults to "Other" and treats your output like static files. It does not generate the routing metadata or intelligent serving that your framework needs.
Go to your Vercel Project Settings. Click "Build & Development Settings". Look for "Framework Preset". If it says "Other", Vercel is flying blind.
Change it to match your stack:
- Next.js (App or Pages Router): Select "Next.js". Vercel will use next/config to understand your routes.
- React with React Router: Select "React". Vercel treats it as a SPA and looks for
index.html. - Remix: Select "Remix". Vercel uses Remix's routing conventions.
- Vite: Leave it as "Other" (then add
vercel.jsonwith the rewrite rule; see Fix #3). - Gatsby: Select "Gatsby".
- SvelteKit: Select "SvelteKit".
- Hugo, Jekyll, or static HTML: Select "Static Site" or leave as "Other".
If you are not sure, check your package.json. Look at the build script in the scripts section. "build": "next build" means Next.js. "build": "vite build" means Vite. "build": "react-scripts build" means Create React App (a React SPA). "build": "gatsby build" means Gatsby.
After changing the preset, redeploy by clicking the "Redeploy" button in the Deployments tab. Do not re-push to Git; Vercel will re-run your build with the new preset. Wait for the deployment to finish. Check again.
If the 404 persists after a redeploy, your output directory setting is likely wrong.
Fix #2: Verify Output Directory & Build Settings
Vercel expects your build to write output to a specific folder. If you configured the wrong folder, or if your build script is wrong, Vercel serves an empty directory and gets 404.
The default output directories by framework are:
- Next.js:
.next - React (with build script):
build(Create React App) ordist(Vite) - Vite:
dist - Gatsby:
public - SvelteKit:
.svelte-kit - Static:
.(root)
Go to your Project Settings in Vercel. Click "Build & Development Settings". Look for "Output Directory". Make sure it matches where your build actually outputs files.
To test locally, open a terminal and run:
vercel build
This command runs your build script (the one in package.json) and simulates Vercel's build step. It outputs to .vercel/output/. After it completes, open .vercel/output/static. Do you see your assets there? HTML, CSS, JavaScript, images?
If the folder is empty or missing, your build script is broken or does not exist. Check:
- Does your
package.jsonhave abuildscript? Vercel runsnpm run buildby default. - Does the script actually produce output? Run it locally:
npm run build. Where does it write files? - Is your
.gitignoreexcluding the output folder? If so, Vercel will not see it.
Once you confirm your output directory setting matches reality, redeploy. If the 404 persists, move to Fix #3 (SPA) or Fix #4 (Next.js dynamic routes).
Fix #3: Add SPA Fallback (React/Vite Apps)
If your app is a React SPA or Vite SPA (client-side routed, not server-side rendered, not Next.js), Vercel does not know that all routes should serve the same index.html file.
Here is the problem: A user clicks a link to /about in your React app. The browser sends a request to https://yoursite.com/about. Vercel looks for a file named /about. It does not exist (there is no /about.html). Vercel returns 404. But in your browser, /about is not supposed to be a file. It is handled by JavaScript inside your React app. Your router matches /about to a component and renders it.
Vercel does not know this. It sees no /about file, so it returns 404 before your JavaScript even loads.
The fix: Create a vercel.json file in your repo root:
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}
This tells Vercel: "If you cannot find a file, serve /index.html instead." Vercel will send every request to /index.html. Your React router (React Router, Remix, whatever you use) then reads the URL and renders the correct component.
Commit and push the file. Redeploy. Now:
- User clicks
/about. - Vercel looks for
/aboutfile. Does not exist. - Vercel serves
/index.htmlinstead. - Browser loads HTML and runs JavaScript.
- Your router runs. Sees
/aboutin the address bar. Renders the About component.
Lovable projects need this rewrite rule. Most Vite projects need this. Any client-side router needs this. Do not add this rule if you are using Next.js or a server-rendered framework. They handle routing differently.
Fix #4: Configure Dynamic Routes (Next.js)
Next.js has two modes for dynamic routes: static generation (SSG) at build time and dynamic rendering at request time.
If you have a route like /products/[id] and you did not tell Next.js which [id] values exist at build time, Next.js will 404 in production. On localhost, Next.js falls back to server-side rendering. You request /products/123, Next.js renders it on the fly. On Vercel, without pre-generation, there is no fallback. No file, no render, 404.
Fix this by adding generateStaticParams() to your dynamic route:
export async function generateStaticParams() {
return [
{ id: '1' },
{ id: '2' },
{ id: '3' },
];
}
export default function ProductPage({ params }) {
return <h1>Product {params.id}</h1>;
}
This tells Next.js to pre-generate /products/1, /products/2, /products/3 at build time. Requests to those routes work. Requests to /products/999 still 404 (expected).
If you have hundreds of products and cannot list them all at build time, use Incremental Static Regeneration (ISR):
export const revalidate = 3600; // Revalidate every hour
Or use dynamic rendering (slower, but always works):
export const dynamic = 'force-dynamic';
This tells Next.js to render every request on the server, no pre-generation. It is slower (no caching) but eliminates this class of 404. Check your next.config.js and next/image usage too. If you are using next/image with a custom domain, you need to configure a loader in next.config.js so Vercel knows how to optimize the image.
Fix #5: Verify Domain & DNS (Custom Domain 404s)
If your .vercel.app URL works but your custom domain returns 404, your DNS is the problem, not your app.
Vercel assigns an IP address or hostname to your deployment. Your custom domain has to point there via DNS records. Go to your Vercel Project > Settings > Domains. Vercel shows your custom domain and the exact DNS record you need to add.
If Vercel shows an A record, add a record in your domain registrar (GoDaddy, Namecheap, wherever you bought the domain):
- Type: A
- Name: @ (or your subdomain, e.g., www)
- Value: The IP Vercel shows (e.g., 76.76.19.165)
If Vercel shows a CNAME record:
- Type: CNAME
- Name: @ or www
- Value: The hostname Vercel shows (e.g., cname-abc.vercel.dns.abc.com)
Most registrars recommend CNAME for subdomains and A for apex domain (@). Vercel accepts both. After adding the record, DNS propagation takes 5 minutes to 48 hours. Usually 5-15 minutes. To check:
nslookup yourdomain.com
Does the result match Vercel's expected IP? If not, propagation is still happening or you entered the record wrong. If you get an SSL certificate error, wait 30 minutes. Some registrars block DNS changes during validation.
When to Stop Troubleshooting: Managed Hosting Alternative
You have tried all five fixes. The 404 persists. The problem might be a framework incompatibility, an undocumented Vercel limitation, a Node.js version mismatch, or an experimental Next.js feature that Vercel does not support.
At this point, staying on Vercel means ongoing debugging. Managed platforms like Ship remove this entire class of problem. Ship is a PaaS (Platform-as-a-Service) for hosting apps. You upload or connect your Git repo, Ship detects your framework (Next.js, Vite, Remix, Lovable, etc.), and your app is live. No vercel.json. No framework presets. No output directory fiddling.
Ship charges flat-rate pricing: one price per month, regardless of traffic. No overage charges. No pay-per-request meters. For a team building Lovable apps or shipping multiple Vite projects, the certainty is worth more than the raw hourly cost. You spend less time in config, more time shipping. You can also explore self-hosted alternatives to Vercel if you want full control.
Frequently Asked Questions
My Next.js app works on localhost but returns 404 on Vercel. I did not change anything.
Next.js localhost falls back to server-side rendering for any route it did not pre-generate. Vercel does not have that fallback. Add generateStaticParams() to your dynamic route, or set dynamic = 'force-dynamic' to render every request on the server.
My .vercel.app URL works fine but my custom domain is 404. What is wrong?
DNS configuration. Your custom domain is not pointing to Vercel's servers. Go to Project Settings > Domains. Add the A or CNAME record Vercel shows. Wait 5-48 hours for propagation. Check with nslookup yourdomain.com.
I deployed a React app and every route except / returns 404. Help.
You need an SPA fallback. Add a vercel.json file in your repo root with the rewrite rule shown in Fix #3. Commit, push, and redeploy.
Why does the Vercel build say "Deployment Ready" if something is wrong?
The build step succeeded (your code compiled and files were written). Routing metadata is evaluated afterward. Vercel does not fail the deployment if routing is broken. The build and routing are separate concerns.
Can I have a 404 if my framework preset is correct?
Yes. Your output directory might be wrong, or you might be missing an SPA fallback. Run the Two-URL Test: if your .vercel.app works, DNS is fine. If it does not, check Fix #1 or #2.
I changed the framework preset but the 404 persists. What now?
Confirm your output directory setting. Run vercel build locally and check .vercel/output/static. If assets are there, check your vercel.json or next.config.js for routing rules. If they are not there, your build script is broken.
I have an old app that was working and now it is 404. I did not change the code.
Vercel changed something, or a regional infrastructure issue occurred. Check the Deployment History tab. Look at the build logs for the failing deploy. If the build output looks right, the issue is routing or DNS. If the build output looks wrong, something broke in your build script or Node.js version.
Does vercel.json slow down my app?
No. Rewrites happen at the HTTP layer before your app code runs. No performance cost.
The Bottom Line
A 404 after "Deployment Ready" means Vercel built your app successfully but does not know where to send a request. Use the Two-URL Test to isolate whether the problem is framework preset, output directory, routing config (SPA fallback), dynamic routes, or DNS. Each has a specific, one-sentence fix.
If you want to stop debugging framework configuration entirely, managed hosting like Ship eliminates this entire class of problem. You point your repo at the host, it detects your framework, and your app works.