Skip to content

It works locally but not on Vercel: 10 causes and fixes

Find your symptom, get the cause, the fix and the check that proves it. Plus a pre-flight for before you push and how to read a build log.

15 min read

Your code is probably fine. Deploying is a house move.

Your laptop is a room where everything the app needs is lying within reach, because you put it there over months without noticing. Secret keys in a file. A database running in the background. An uploads folder. A dev server that forgives mistakes. The server is an empty room, and only the app traveled.

That one fact is all ten causes below. Each comes with the fix and, more importantly, the check that proves the fix landed, because “I added the environment variables” and “the environment variables are there” are different facts, and the gap between them is where a second failed deploy lives.

The examples name Vercel because that is where most people meet this, but nothing here is specific to one host. The same ten happen on Netlify, Render, Railway, Fly and Cloudflare, because they are all the same move from a laptop to a server.

Start here: what are you looking at?

If the site is broken right now, find your symptom. If nothing is broken yet, skip to the pre-flight.

What you see Usually this Cause
A completely blank white page Something crashed as the page loaded. The browser console’s first red line names it 1 or 2
A 500 error page It crashed on the server, so the browser console will not help. Read the host’s function logs 1, 3 or 5
Build failed: cannot resolve ./Button A capital letter in a filename 6
Build failed, but the dev server was perfectly fine Dev mode was forgiving something 10
Pages load, but every list is empty It is not reaching the database, or the tables are not there 1, 3 or 5
Everything works except one action, which hangs then errors That action takes longer than the server allows 8
The console says “blocked by CORS policy” The frontend and backend are on different domains now 9
Uploads work, then the file is gone later It was written to a disk that does not persist 4
“Relation does not exist”, or “no such table” The production database has an older shape 5
Login works locally and fails in production The new domain is not registered with your login provider See the two below
The build succeeded but the site shows the old version Wrong branch, or a cache See the two below

Before you change anything, get the actual error. The white page in front of you is a symptom, not the message. Reading a build log is further down.

Two failures that are not in the ten

Login redirects. Many login providers only return to addresses you have registered with them, and your production domain is not one of them until you add it. It looks like a mysterious login failure and it is usually a two-minute settings change.

Wrong branch, or a cache. Before debugging anything, confirm the deployment you are looking at is the commit you think it is. Compare the deployment’s commit against your latest one, then hard-reload the page. A fair share of “it did not work” turns out to be “it did not deploy”.

The pre-flight: ten minutes before you push

Run this before you deploy. It prevents most of what follows.

  • ☐ 1. Run the real build locally, not the dev server. Production mode refuses things dev mode allows.
  • ☐ 2. Run the built version and click through it. Building is not running. Start the production build on your machine and use the app for two minutes.
  • ☐ 3. List every environment variable your code reads, and confirm each one is set on the host. Not from memory: the prompt for the list is in cause 1.
  • ☐ 4. Search the project for localhost and 3000. Every hit is a bug that has not happened yet.
  • ☐ 5. Confirm which database production points at, and that migrations run as part of the deploy.
  • ☐ 6. Confirm the Node version is pinned and matches what the host will use.
  • ☐ 7. Say your rollback step out loud. If you cannot, you do not have one. Find it now rather than later.
Terminal
npm run build
npm run start

Those are the usual commands. If your project uses different ones, they are in the scripts section of package.json, and your AI will name them if you ask.

One more, and it is not technical: deploy on a Tuesday morning rather than a Friday night. Nothing on this page helps much at 11 PM when you are the only person awake.

Causes 1 to 5: what your laptop had and the server does not

1. The environment variables are not on the server

Why it happens: your local environment file is deliberately not in the repository, so it does not travel. The app arrives with nowhere to read its keys from.

The fix: add every variable in the hosting dashboard, once, by hand.

Prompt
List every environment variable this project reads anywhere in the code,
with the file it is read in and what it is for. Names only, never print a
value. Mark the ones the app cannot start without.

The check: compare that list against the host’s settings side by side, name by name. Then redeploy. Many hosts only apply new variables to a new build, which is why people add them and still see the same error.

2. A localhost address is hardcoded somewhere

Why it happens: localhost is your own machine’s address. On the server there is no such place, so the app is knocking on a door that does not exist.

The fix: every hit becomes an environment variable.

Prompt
Search the whole project for "localhost", "3000" and "127.0.0.1". Show me
every hit with its file and what it is doing. Then replace each one with an
environment variable and show me the diff before applying it.

The check: run the search again afterwards and confirm there are no hits left outside comments and documentation.

3. Production uses a different kind of database

Why it happens: a file-based database on your laptop and a hosted Postgres in production are not the same thing wearing different names. They behave differently, and code that works on one can fail on the other.

The fix: use the same kind of database in both places, with a separate copy for local work.

The check: ask what each environment connects to and confirm the two are the same type. Then confirm your local copy really is separate. If local development points at the live database, a test delete removes real customer rows.

4. Files are saved to the server’s disk

Why it happens: serverless hosting has no lasting disk. A file written during one request can be gone by the next. It works locally because your laptop has a floor.

The fix: uploads go to an object storage service, never to a folder inside the project. Your backend platform most likely offers one.

Prompt
Find every place this project writes a file to disk. For each one, tell me
whether that file needs to exist after the request finishes. Move the ones
that do to object storage and show me the diff.

The check: upload a file in production, wait ten minutes, reload, then open it. The wait is the test. Straight after an upload it will often still work, which is exactly what makes this one easy to miss.

5. The migrations never ran

Why it happens: you changed the shape of the database on your laptop. Production still has last month’s shape, and the code is asking for columns that are not there.

The fix: run migrations as part of the deploy rather than by hand. A manual migration gets forgotten exactly once, and it is always an important one.

The check: the deploy log should show the migration step running. If you cannot find it in the log, it did not run. It was never asked to.

Do this once, before you need it: find out how to undo a migration, and write the command down somewhere that is not your terminal history.

Causes 6 to 10: what the server does differently

6. A filename’s capital letter

Why it happens: macOS treats Button.tsx and button.tsx as the same file by default. Linux, which your server almost certainly runs, treats them as two different files. So an import that works on your laptop points at nothing on the server.

The fix: match every import to the real filename, character for character.

Prompt
Check every import in this project against the real filename on disk,
including capital letters. List every mismatch. This has to be exact: the
server is case-sensitive even though my laptop is not.

The check: the build log names the file it could not resolve. Compare that name letter by letter with the file in your project. The difference is usually one capital.

7. The Node version does not match

Why it happens: your laptop runs one version and the host picks another, and code that is fine on one can fail to build on the other.

The fix: pin the version in package.json so the host uses what you use.

The check: run node -v locally, then find the Node version in your deploy log. They should match. This takes thirty seconds and explains a surprising number of “it built yesterday” failures.

8. The work takes too long and the server gives up

Why it happens: your function takes forty seconds. Hosted functions are stopped after a time limit, often far shorter than you would guess. It never happened locally because your laptop has no such limit. Search your host’s documentation for “function timeout” to find yours.

The fix: move slow work to a background job and return an answer immediately.

Prompt
Find every route or function here that could take more than a few seconds:
anything looping over many records, calling an AI model, processing a file
or sending many emails. For each, tell me roughly how long it takes and
whether it should move to a background job.

The check: this is not only about the limit. Someone watching a spinner for eight seconds already believes the app is broken. Anything much over a few seconds wants a background job and a visible status.

9. CORS blocks the browser

Why it happens: on your laptop the frontend and the backend were the same address. In production they can be different domains, and a browser will not let a page on one domain call a server on another unless the server says that origin is allowed.

The fix: allow your real domains by name.

Prompt
Where is CORS configured in this project? Show me the allowed origins.
Replace any wildcard with my production domain and my local development
address, nothing else.

The check: the browser console names the origin it blocked, and that exact string is what belongs on the allowed list. Do not leave a * there: a wildcard lets a page on any site call your API from a browser, and it is not the fix for a blocked origin anyway.

10. Dev mode was hiding the errors

Why it happens: the dev server exists to keep you moving, so it tolerates things the real build refuses. The first time the strict version of your project ever runs is on the server, in front of users.

The fix: run the real build locally before every deploy, then start the built version and click through it.

The check: this is cause ten because it catches most of the other nine before you push. If you take one habit from this article, take this one. It costs about a minute and it moves failures back to your laptop, where they are free.

How to read a build log without reading code

A failed deploy hands you a few hundred lines of text. Almost all of it is noise. Five rules find the signal.

  1. Go to the first error, not the last. Logs scroll, so the last error is the one you see, and it is usually a consequence of the first. Search the log for the first Error and start there.
  2. Warnings are not errors. Every real project has warnings. Ignore them today.
  3. The useful line has a file path and a number in it, something like ./src/components/Button.tsx:14. That line tells you where. A line of pure prose with no path is usually commentary.
  4. “Command failed with exit code 1” is not the error. Neither is “Build failed”. They are announcements that there was one, and they sit at the very bottom, which is exactly where people look. Scroll up from them.
  5. Copy generously: about twenty lines above the first error and five below. One line out of context is why the answer comes back as a guess.

Then paste it with the suspects named:

Prompt
This is my deploy log and the build failed. Do not change any code yet.
Tell me which line is the actual error, what it means in plain English,
and which of these it is: a missing environment variable, a hardcoded
localhost, a case-sensitive filename, a Node version mismatch, a migration
that did not run, or something else. Here is the log: [paste]

Naming the usual suspects is the trick. It turns a general lecture into a diagnosis.

One more thing worth knowing: a build that succeeds and a site that works are different events. If the build was green and the page is still broken, the problem is at runtime rather than build time, so stop reading the build log and open your host’s function logs or the browser console instead. The symptom table at the top says which.

Set up once, then check every deploy

Do this once per project Why
Every environment variable added on the host They do not travel with the code
Migrations wired into the deploy step So they cannot be forgotten
The Node version pinned So the host stops choosing for you
Object storage for uploads Because the server’s disk is not permanent
CORS set to your actual domains So you never ship a wildcard
Login redirect addresses registered for production The one that is not in the ten
A rollback step, written down The only item here that matters at 3 AM

Then, every single deploy:

  1. The real build passes locally.
  2. You start the built version and walk the main flow once.
  3. Any new environment variables are added on the host, and you redeploy after adding them.
  4. Deploy.
  5. Open the live site and do the main task yourself, on your phone, not only in the desktop tab you already have open.

Step 5 is the one people skip, because the dashboard said “Ready”. Ready means it deployed, not that it works. The same habit runs through 20 things to fix before a client touches your app.

Before you push, this prompt does the whole sweep:

Prompt
Read the whole project. List everything that will behave differently on my
host than on my laptop: environment variables, file paths, database, file
writes, timeouts, CORS, Node version and anything case-sensitive. For each
one, tell me what I need to do before I deploy. Do not change any code yet.

And after a failure, when you are not sure it is really fixed:

Prompt
I fixed [what you changed]. Before I redeploy, tell me what else in this
project has the same problem somewhere else. List all of them.

That second one matters more than it looks. These causes are patterns rather than incidents. One hardcoded localhost is almost never the only one.

What this does not cover

This is about the gap between a laptop and a server. It will not help with problems that only appear under real traffic, and it will not tell you whether the app is any good. For what a live AI feature costs once real users arrive, see AI cost control. For whether the work itself holds up, see how to review AI code without reading it.

Your laptop is not lying to you on purpose. It lied kindly, for months, by keeping everything you needed within reach. Production is just the first place that tells you the truth.

Common questions

Why does my app work locally but not in production?
Because only the app traveled. Your laptop accumulated everything the app needs over months, secret values, a database, files on disk, a forgiving dev server. The production server starts empty and runs the strict build. Almost every deploy failure is one of those missing pieces rather than a bug in your code.
Why is my deployed site a blank white page?
Something crashed while the page loaded. Open the browser console and read the first red line. The usual causes are a missing environment variable and a hardcoded localhost address. If you see a 500 error page instead, it crashed on the server, so read your host’s function logs rather than the browser console.
I added the environment variables and the deploy still fails. Why?
Many hosts only apply new variables to a new build, so an existing deployment keeps running without them. Redeploy after adding them. If it still fails, ask your AI for the list of every variable the code actually reads and compare that list against the host’s settings name by name.
How do I read a build log if I cannot read code?
Find the first error rather than the last, ignore warnings, and look for the line with a file path and a line number in it. A line saying the build failed, or that a command exited with code 1, is an announcement rather than the error itself, so scroll up from it. Then paste about twenty lines around the first error into your AI and ask which line is the real error.