Works Locally, Fails in Production

Works locally, breaks on Vercel or Render. Diff env vars, runtime version, filesystem case-sensitivity, network, and build process — the systematic checklist.

The classic pain: npm run dev is perfect locally. Deploy to Vercel / Netlify / Fly.io / Render — build fails, runtime crashes, or the page seems to load but every feature is broken. “Works on my machine” is the oldest joke in the book, but the root cause is always the same family: implicit local assumptions don’t hold in production.

The fix isn’t guessing — it’s systematically diffing local vs prod across env vars / runtime version / filesystem / network / build process.

Common causes

Ordered by hit rate, highest first.

1. Env vars not set in production

Most common. Your local .env has DATABASE_URL, STRIPE_SECRET_KEY, but the deploy platform doesn’t — process.env.X === undefined at runtime.

How to spot it: Prod logs show Cannot read property X of undefined or Invalid URL.

2. Node / Python version mismatch

Local Node 22, prod Node 18 (platform default). Newer ES2023+ syntax or APIs like Array.prototype.toSorted don’t exist on 18 — runtime crash.

How to spot it: Prod logs show X is not a function or SyntaxError.

3. Filesystem differences (case sensitivity, path separators)

// macOS / Windows: case-insensitive
import './components/Button';  // ✅ even if file is button.tsx

// Linux prod: case-sensitive
import './components/Button';  // ❌ Cannot find module

How to spot it: Prod build errors “Module not found” but local works.

4. Local-only services (filesystem, in-memory state)

Locally you do fs.writeFile('./uploads/x.jpg'). On serverless platforms (Vercel / Cloudflare Workers) the filesystem is read-only — write throws.

How to spot it: EROFS: read-only file system or similar.

5. Dev server vs prod build behave differently

Vite / Webpack dev mode has HMR, looser parsing. Prod build tree-shakes, minifies, SSRs. Code that works in dev can break in prod build.

How to spot it: Run npm run build && npm start locally — does it reproduce?

localhost:3000 vs yourdomain.com hit different CORS rules, different cookie SameSite behavior, different allowed-origin lists. OAuth was using test keys in dev and still is in prod.

How to spot it: Network shows CORS error / 401 / 403 in prod but works in dev.

7. Timezone / locale

date.toString() shows “Beijing time” locally, prod server is UTC — every time display is off by 8 hours.

How to spot it: Time-related features are weird in prod.

Shortest path to fix

Step 1: Run the prod build locally

90% of “local works, prod doesn’t” reproduces here:

# Next.js
NODE_ENV=production npm run build && npm start

# Vite
npm run build && npm run preview

# Generic
NODE_ENV=production node server.js

If local prod build also fails → it’s a build / code issue, not deployment. Fix it.

Step 2: Diff env vars

# List local
cat .env | sort > /tmp/local-env.txt

# List prod (from platform dashboard, manually)
echo "DATABASE_URL=..." > /tmp/prod-env.txt
sort /tmp/prod-env.txt > /tmp/prod-env-sorted.txt

# Diff
diff /tmp/local-env.txt /tmp/prod-env-sorted.txt

Add missing:

# Vercel
vercel env add DATABASE_URL production
vercel env add STRIPE_SECRET_KEY production

# Or via dashboard

Step 3: Lock runtime version

// package.json
{
  "engines": {
    "node": "20.x"
  }
}

Vercel / Netlify respect engines.node. Cloudflare Pages: set NODE_VERSION=20 in Build settings.

Confirm .nvmrc / .node-version:

20.10.0

Step 4: Fix case sensitivity

# On macOS, list every import and check casing
grep -r "from './" src/ | sort -u | head -50

# Compare against actual files
ls -la src/components/

Force case-sensitive volume (extreme but bulletproof):

diskutil apfs createContainer disk0s2
# Create a case-sensitive volume and move the project there

Or use eslint rules import/no-unresolved + import/case-sensitive.

Step 5: Replace local-only dependencies

Local                 → Production
================================================
fs writeFile          → S3 / R2 / Cloudinary
in-memory cache       → Redis (Upstash / Cloudflare KV)
SQLite                → Postgres / Turso
local cron            → Vercel Cron / Inngest
process.env at boot   → remote config / Vault

Step 6: dev / prod behavior parity

// Never fork dev/prod logic
if (process.env.NODE_ENV === 'development') {
  // ❌ dev can't catch prod bugs
}

// Instead:
const useMock = process.env.USE_MOCK === 'true';
// Then run staging with USE_MOCK=false to validate

Step 7: UTC + explicit locale

// Server-side: always UTC
const date = new Date(); // transport as ISO string

// Client-side display: use Intl
new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York' }).format(date)

Prevention

  • Run npm run build && npm start locally before every PR — don’t trust the dev server
  • Validate env vars with zod at boot — fail-fast on missing rather than at runtime
  • Lock engines.node in package.json; CI uses actions/setup-node@v4 with: node-version-file: .nvmrc
  • Mac users: run builds inside a Linux Docker container to mimic prod case sensitivity
  • Any “local-only” code (fs.write, in-memory state) gets replaced with a cloud equivalent on day 1
  • Maintain a preview / staging env identical to prod config; validate every PR there before merge
  • Store times as UTC ISO strings; convert to timezone only at display
  • Whole company on the same .nvmrc / .python-version — kill “works on my machine”

Tags: #Backend #Debug #Troubleshooting