Engineering guide
Why Applications Work Locally but Fail in Production
"It works on my machine" is one of the most expensive sentences in software. Production differs from a laptop in configuration, operating system, network, data and scale, and any one of those can break an application that passed every local test. This guide covers the common causes with the exact error messages you are likely to see, a symptom-to-cause table, a troubleshooting sequence and the practices that prevent repeats.
- Level
- Technical
- Best for
- Developers, DevOps engineers and IT managers responsible for live systems
- Assumes
- You deploy or support web applications and can read server logs.
Key takeaways
- Most production-only failures come from configuration, dependencies, network access, caching or the deployment itself, not from the application logic.
- The error text usually tells you which: ETIMEDOUT means blocked, ECONNREFUSED means nothing listening, 502 means the app is down, 504 means it is too slow.
- Build once and promote the same artifact through every environment.
- Validate configuration at startup so a missing setting stops the app loudly instead of breaking it quietly.
- Deploy to every server or none, smoke-test immediately, and keep rollback one step away.
Why "it works on my machine" happens
A developer's machine has years of accumulated tools, an open network, a small friendly database and often a different operating system from the server. Production has stricter security, real data, real traffic and usually several servers behind a proxy. The code may be identical; everything around it is not.
The most common causes, and what they look like
1. Configuration differences
Settings that exist locally but not in production, or with different values: environment variables, API base URLs, feature flags, missing secrets. Two classics: ASP.NET Core treats an unset ASPNETCORE_ENVIRONMENT as Production, so settings that only exist in appsettings.Development.json silently disappear; and front-end builds that bake the API address in at build time, so a production build pointing at localhost fails for every user except the developer.
2. Missing or different dependencies
- No lock file, or builds using
npm installinstead ofnpm ci, so production quietly installs newer versions than were tested. - A different runtime version on the server; pin it with
"engines"in package.json orglobal.jsonfor .NET. - Native modules (image processing, password hashing) compiled on Windows or macOS and copied to a Linux server. Install dependencies on the target platform or in the build container, never copy
node_modulesbetween machines. - Command-line tools or system libraries installed on the developer's machine years ago and never on the server.
3. File systems and paths
Windows and macOS usually ignore filename case; Linux does not. An import of ./Components/Header works on a laptop and fails on the server with Error: Cannot find module './Components/Header' because the folder is actually components. Hard-coded backslashes and missing write permissions on upload or log folders cause similar production-only failures.
4. Database connectivity
ETIMEDOUT(or "connection timed out"): packets are being dropped; check firewalls, cloud security groups and network ACLs between app and database.ECONNREFUSED: the host answered but nothing is listening on that port; check the host name, port and that the database is running.- TLS errors: managed databases often require encrypted connections that local databases do not; enable SSL in the connection string.
- Pool exhaustion under load: requests hang, then fail, only when real traffic arrives; usually connections not being returned, or a pool too small for the number of app instances.
- Migrations not applied: "column does not exist" errors right after a deploy.
5. Caching
Browsers and CDNs can keep serving old JavaScript and CSS after a deploy, so some users run a mix of old and new code. The standard fix is content-hashed asset names cached for a long time, with HTML that must be revalidated on every visit. This site uses exactly that: pages are sent with Cache-Control: no-cache, and scripts have hashed names cached for a year, so every release reaches every visitor on their next page load.
6. APIs, browsers and proxies
| What you see | Likely cause |
|---|---|
blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present | The API allows localhost but not the production domain. |
Mixed Content: ... requested an insecure resource | An HTTPS page calling an http:// API or asset; browsers block it. |
| Endless redirect loop after enabling HTTPS | The proxy terminates TLS but does not forward X-Forwarded-Proto, so the app keeps redirecting "to HTTPS." |
| Users logged out on every request | Cookies set with SameSite=None must also be Secure; browsers drop them otherwise. |
| Payments or emails fail only in production | The third party's live service behaves differently from its sandbox, or production keys were never configured. |
7. Deployment and build problems
A development build deployed by mistake, a build step skipped, the wrong folder uploaded, a process that was never restarted, or a port already in use. Single-page apps add their own classic: deep links work in development but return 404 in production because the web server is not configured to serve the app for those paths. On this site, the frontend deploy also needs a prerender step, and the API runs under a process manager that must be restarted after new files are copied. Both are on a written checklist, because both are easy to forget.
8. Resources, time and scale
- Time zones: servers usually run in UTC. A "today's orders" report using server time misses the last four to five hours of a US Eastern business day.
- Timeouts: an AWS Application Load Balancer closes idle connections after 60 seconds by default, so a long report that works locally returns
504in production. - Memory limits: a container that exceeds its memory limit is killed (exit code 137, "OOMKilled") with no application error at all.
- Concurrency: race conditions and locking problems only appear with many simultaneous users.
A step-by-step troubleshooting process
| Symptom | Look first at |
|---|---|
502 Bad Gateway | The app process: crashed, not started after deploy, or listening on a different port than the proxy expects. |
504 Gateway Timeout | Slow requests versus proxy or load balancer timeouts; slow database queries. |
403 Forbidden | File permissions, a web application firewall rule, or missing credentials. |
| Works for some users, not others | One bad server behind a load balancer, stale caches, DNS still pointing at the old host. |
| Works after a restart, fails again later | Memory leaks, connection pool exhaustion, expiring tokens or certificates. |
- CaptureExact error, time, who is affected
- LogsApp, web server, platform
- CompareConfig, versions, data
- ReproduceProduction-like environment
- IsolateTest each dependency directly
- Fix and guardAdd a test or check
- Capture the symptom precisely: the exact message, time, URL or action, and whether it affects everyone or some users.
- Read the logs at every layer: application, web server or proxy, and hosting platform; line them up by timestamp.
- Compare environments: runtime and package versions, configuration values (without printing secrets) and what changed in the last deploy.
- Reproduce in staging or a container built from the same image as production.
- Isolate: call the API directly, test the database port, check DNS and certificate expiry.
- Fix it and add a guard (a test, a startup check or an alert) so the same failure cannot return silently.
node -v; dotnet --info | head -n 5 # runtime versions
curl -sI https://api.example.com/health # reachable? over HTTPS? which status?
nc -zv db.internal 5432 # can this server reach the database port?
journalctl -u myapp --since "15 min ago" # recent service logs (systemd)
nslookup api.example.com # does DNS resolve where you expect?
echo | openssl s_client -connect api.example.com:443 2>/dev/null | openssl x509 -noout -enddate # certificate expiryHow to prevent production-only failures
- Build once, deploy many. One artifact from CI, promoted unchanged from staging to production.
- Keep environments alike with containers and infrastructure as code.
- Keep configuration out of code and validate it at startup.
- Deploy to all servers or none, and verify every instance reports the new version.
- Smoke-test immediately after every deploy: home page, sign-in, one key transaction, one API call.
- Observe: structured logs, error tracking, and alerts on error rate and latency.
- Release safely: feature flags, gradual rollouts and a rollback you have actually practiced.
const required = ['DATABASE_URL', 'API_BASE_URL', 'SESSION_SECRET'];
const missing = required.filter((name) => !process.env[name]);
if (missing.length) {
console.error(`Missing required settings: ${missing.join(', ')}`);
process.exit(1);
}builder.Services.AddOptions<PaymentOptions>()
.Bind(builder.Configuration.GetSection("Payments"))
.Validate(o => !string.IsNullOrWhiteSpace(o.ApiUrl), "Payments:ApiUrl is required")
.ValidateOnStart();Frequently asked questions
Why does the app work in staging but not in production?
Usually because staging is not a faithful copy: different configuration, smaller data, fewer servers, or missing security rules such as firewalls and TLS requirements. Comparing the two environments setting by setting normally reveals the gap.
Should we use Docker to avoid these problems?
Containers remove a large class of dependency and operating-system differences because the same image runs everywhere. They do not fix configuration, network or data differences, which still need staging and startup checks.
How can we debug production without direct access?
Through good logs, error tracking and metrics, plus a staging environment that mirrors production. Health-check endpoints and a request ID carried through every log line make it possible to follow one request across services.
What is the fastest first check when something breaks after a deploy?
Look at what changed: the deployment itself, configuration and dependency versions. If the impact is serious, roll back first and investigate in staging.
Why do some users still see the old version after a deploy?
Browser or CDN caching. Use content-hashed file names for scripts and styles and make HTML revalidate on every visit, so new releases reach everyone on their next page load.
How CodeMutual can help
Discuss your project with CodeMutual
Tell us what you're working on. We'll come back with a plain-English plan within one business day, and you'll talk directly to the engineers who would do the work.