Back to blog

Local Dev vs Production: What New Devs Don't See

web developmentdevopsdeploymentbackenddocker
Local Dev vs Production: What New Devs Don't See

You run npm run dev, open localhost:3000, and everything works. The page loads instantly. The API responds in 2 ms. No errors. Ship it, right?

Then you deploy to production and things start breaking in ways you never expected. API calls that took 2 ms now take 200 ms. Images that loaded fine locally return 403 errors. The database connection drops randomly. Users report bugs you can't reproduce.

This post maps out every major difference between your local dev environment and a real production setup. Not to scare you — but so you know what's coming and can prepare for it.

The Big Picture

Local dev is one machine talking to itself. Production is a distributed system with CDNs, load balancers, multiple servers, managed databases, and users connecting from every network condition imaginable.


1. Networking

Local

  • Everything runs on localhost — zero network latency
  • No DNS resolution needed
  • No firewalls, proxies, or NAT between your frontend and backend
  • HTTP works fine (no HTTPS)
  • CORS doesn't matter when everything is same-origin

Production

  • Users are 50–500 ms away depending on geography
  • DNS resolution adds 20–100 ms on first request
  • Multiple network hops: CDN → load balancer → server → database
  • HTTPS is mandatory (TLS handshake adds ~50 ms)
  • CORS must be configured correctly or browsers block requests
  • Firewalls and security groups restrict which ports/IPs can connect

What catches new devs off guard

Your API that responded in 2 ms locally now takes 150 ms because the user is in Southeast Asia, the server is in us-east-1, and there's a TLS handshake, DNS lookup, and three network hops between them.


2. Security

Local

  • No authentication needed to access your dev server
  • API keys hardcoded in .env.local — it's fine, only you see them
  • No rate limiting
  • No input validation? No problem, you're the only user
  • HTTP everywhere
  • No CSP headers, no HSTS, no security headers at all

Production

  • HTTPS with valid TLS certificates (Let's Encrypt, Cloudflare, etc.)
  • Secrets stored in environment variables or a vault (never in code)
  • Rate limiting on all public endpoints
  • Input validation and sanitization on every user-facing endpoint
  • CORS, CSP, HSTS, X-Frame-Options, and other security headers
  • DDoS protection (Cloudflare, AWS Shield)
  • Authentication and authorization on every protected route
  • SQL injection and XSS protection

The gap

AspectLocalProduction
HTTPSNot neededMandatory
Secrets.env.local fileVault / env vars in platform
Rate limitingNonePer-IP, per-user, per-endpoint
Input validationOptionalCritical
Security headersNoneFull suite
AuthUsually skippedEvery protected route
DDoS protectionNoneCDN + WAF

3. Database

Local

  • SQLite file or a single Postgres container
  • One connection, one user (you)
  • No connection pooling needed
  • Migrations run manually: npx prisma migrate dev
  • Data is throwaway — drop and recreate freely
  • No backups needed
  • Full admin access

Production

  • Managed database (AWS RDS, Neon, PlanetScale, Supabase)
  • Hundreds or thousands of concurrent connections
  • Connection pooling is essential (PgBouncer, Prisma connection pool)
  • Migrations must be backward-compatible and zero-downtime
  • Data is the most valuable thing — losing it loses the business
  • Automated backups, point-in-time recovery
  • Principle of least privilege: app user has only the permissions it needs

What breaks

The classic first production database error: FATAL: too many clients already. Your local Postgres had no connection limit issues because you were the only client. In production, every serverless function instance opens its own connection.


4. Environment Variables & Configuration

Local

# .env.local — simple and flat
DATABASE_URL=postgresql://postgres:password@localhost:5432/myapp
API_KEY=sk-test-1234567890
NEXT_PUBLIC_API_URL=http://localhost:3000/api

Production

# Secrets managed by platform (Vercel, AWS, etc.)
DATABASE_URL=postgresql://user:****@prod-db.us-east-1.rds.amazonaws.com:5432/myapp?sslmode=require
API_KEY=sk-live-**********  # Rotated quarterly
NEXT_PUBLIC_API_URL=https://chanhle.dev/api
 
# Production-only variables
SENTRY_DSN=https://****@sentry.io/123456
REDIS_URL=redis://****@cache.us-east-1.amazonaws.com:6379
CDN_URL=https://cdn.chanhle.dev

The traps

  • Forgetting to set an env var in production → app crashes on deploy
  • Using localhost URLs in production config → API calls fail silently
  • Exposing secrets by prefixing with NEXT_PUBLIC_ → anyone can see them in the browser
  • Different env var names across environments → works locally, breaks in CI

5. Error Handling & Logging

Local

  • Errors show in the terminal with full stack traces
  • React error overlay shows the exact line number
  • console.log everywhere — it's right there in your terminal
  • Unhandled promise rejections crash the process (and you restart it)

Production

  • Users see a generic "Something went wrong" page
  • Stack traces go to a logging service (Sentry, Datadog, LogRocket)
  • console.log goes nowhere useful (or floods CloudWatch at $$$)
  • Unhandled errors must be caught or they crash the container/function
  • You need structured logging with request IDs to trace issues

What it looks like

Local debugging:

Error: Cannot read properties of undefined (reading 'email')
    at getUserProfile (/Users/you/app/lib/users.ts:42:15)
    at handler (/Users/you/app/api/profile/route.ts:8:20)

You see the file, line number, and the exact problem. Fix it in 30 seconds.

Production debugging:

A user reports: "I click my profile and it shows a white screen"

You check Sentry. Nothing. You check server logs — 500 errors on /api/profile but no stack trace because error handling swallowed it. You add logging. Redeploy. Wait for it to happen again. Check logs. The user's email field is null because they signed up through a social provider that doesn't return email. You never tested that path.


6. Build & Deployment

Local

npm run dev     # Hot reload, fast refresh, source maps
# Changed a file? See it in <1 second

Production

  • Code goes through CI/CD pipeline: lint → type check → test → build → deploy
  • Build takes 2–10 minutes (not 1 second)
  • Rollbacks must be possible if something goes wrong
  • Zero-downtime deployments (rolling updates, blue-green, canary)
  • Build artifacts are optimized, minified, and tree-shaken

Things that only break in production builds

  • Dynamic imports that work in dev but fail after tree-shaking
  • Environment variables missing at build time
  • CSS that looks fine in dev but breaks after minification/purging
  • API routes that work in dev server but not in serverless functions (file system access, long-running processes)

7. Scaling

Local

  • One user: you
  • One process
  • One CPU core (maybe)
  • One database connection
  • If it's slow, you wait

Production

  • Hundreds to millions of concurrent users
  • Multiple server instances behind a load balancer
  • Horizontal scaling: add more servers when traffic spikes
  • Vertical scaling: bigger machines for heavier workloads
  • Auto-scaling based on CPU/memory/request count
  • Caching layers: CDN → Redis → application cache → database

The scaling stack

Your localhost:3000 doesn't need any of this. But in production, if you're not caching, not using a CDN, and running a single server — your app will go down the first time it gets real traffic.


8. Monitoring & Observability

Local

  • You are the monitoring. You see every error, every log, every slow response.
  • Task Manager / Activity Monitor for resource usage.
  • That's it.

Production

  • Uptime monitoring: Is the app responding? (Pingdom, UptimeRobot)
  • Error tracking: What errors are happening? (Sentry, Bugsnag)
  • Application Performance Monitoring: How fast are requests? (Datadog, New Relic)
  • Log aggregation: What happened? (ELK stack, CloudWatch, Grafana Loki)
  • Infrastructure monitoring: CPU, memory, disk, network (Prometheus + Grafana)
  • Alerting: PagerDuty, Opsgenie — wake someone up at 3 AM if the app is down

You can't fix what you can't see

Locally, you see everything. In production, if you didn't set up logging and monitoring, you're flying blind. Users will find bugs before you do.


9. Static Assets & File Storage

Local

// Just read/write files to the local filesystem
import fs from 'fs';
fs.writeFileSync('./uploads/avatar.png', buffer);

Production

  • Local filesystem doesn't persist in serverless/containerized environments
  • Use object storage: AWS S3, Cloudflare R2, Google Cloud Storage
  • Serve through CDN for global performance
  • Image optimization: resize, compress, convert to WebP on the fly (Cloudinary, Vercel Image Optimization)
  • File size limits and virus scanning on uploads

The classic mistake

// Works perfectly locally
app.post('/upload', (req, res) => {
  fs.writeFileSync(`./public/uploads/${filename}`, file.buffer);
  res.json({ url: `/uploads/${filename}` });
});
 
// In production (serverless):
// ❌ File saved to container filesystem
// ❌ Next request might hit a different container
// ❌ Container restarts = file gone forever

10. The Full Comparison Table

AspectLocal DevProduction
URLlocalhost:3000yourdomain.com
HTTPSNoYes (mandatory)
Users1 (you)Hundreds to millions
Latency~0 ms50–500 ms
DatabaseLocal file/containerManaged, pooled, replicated
Env vars.env.localPlatform secrets / vault
ErrorsFull stack trace in terminalLogging service (Sentry, etc.)
Deploymentnpm run devCI/CD pipeline (5–10 min)
ScalingSingle processAuto-scaling, load balanced
FilesLocal filesystemObject storage (S3, R2)
MonitoringYour eyeballsDatadog, Grafana, Sentry
CachingNone neededCDN + Redis + app cache
SecurityMinimalFull stack (WAF, rate limit, CSP)
DowntimeClose your laptop≤ 99.9% SLA expected
RollbackCtrl+ZBlue-green / canary deploys

How to Bridge the Gap

You don't need to set up a full production stack on day one. But you can start closing the gap early:

Level 1: Docker (Week 1)

Run your app in Docker locally. This catches "works on my machine" bugs.

docker compose up -d

Level 2: Environment Parity (Week 2)

Use a real database (Postgres, not SQLite). Use environment variables for all config. Never hardcode URLs.

Level 3: CI/CD (Month 1)

Set up a GitHub Actions pipeline that runs lint, tests, and builds on every push.

# .github/workflows/ci.yml
name: CI
on: push
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint
      - run: npm run build
      - run: npm test

Level 4: Observability (Month 2)

Add Sentry for error tracking. Add basic health checks. Set up uptime monitoring.

Level 5: Production Mindset (Ongoing)

  • Assume every input is malicious
  • Assume any network call can fail
  • Assume any server can restart at any time
  • Assume your database will run out of connections
  • Assume your disk will run out of space

Key Takeaways

The gap between localhost and production isn't a single chasm — it's a dozen smaller gaps across networking, security, scaling, databases, deployment, monitoring, and more.

You don't need to master all of them before shipping your first project. But you do need to know they exist so you're not blindsided when things break in ways that are impossible to reproduce locally.

Start with Docker and environment parity. Add CI/CD early. Layer in monitoring and security as your app grows. Every production incident you survive makes you a better developer.

The developers who grow fastest aren't the ones who write the cleverest code locally — they're the ones who understand what happens after git push.

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.