Your app works locally and breaks in production because the two differ in the ways that matter: production has real data volume and real concurrent users, different config and secrets, and real external services, while your laptop has none of that. Local is a tiny, clean, single-user version of the real thing. The fix is to shrink the gap where you can, and for the rest, watch production so you see the break the moment it happens. Here is what actually differs, and how to catch it.
Why "works locally" means so little
"It works on my machine" is a running joke for a reason: your machine is the easiest possible conditions for your code. One user, you, clicking through happy paths. A handful of rows in the database. No network latency to your own services. Every secret and setting already in place because you put them there. Production is none of those things, and the distance between the two is where bugs live.
This gap is old enough to have a name. The Twelve-Factor App methodology calls it dev/prod parity and identifies three gaps that widen it: a time gap (code can sit for weeks before it ships), a personnel gap (different people write and run it), and a tools gap (different databases, versions, and operating systems between your laptop and the server).1 The wider those gaps, the more your local success tells you nothing about how the code behaves once it is live.
The gaps that actually break things
A handful of differences cause almost all "works locally, breaks in production" bugs. Knowing them tells you where to look.
- Data volume. A query with no index is instant on the hundred rows you have locally and slow on the million in production. This is the single most common one, and we go deep on it in why is my database query slow.
- Concurrency. Code that is fine for one user can fall over when hundreds hit it at once: exhausted connection pools, race conditions, locks. You never see it alone at your desk. More on this in what breaks when your app gets its first real users.
- Real external services. Locally you mock the payment provider or hit a test key. In production it has real latency, rate limits, and occasional failures your code never had to handle.
- Real, messy data. Production has the null field, the emoji in a name, the malformed input a real user typed, that your clean local seed data never contains.
- Different infrastructure. A different database version, operating system, or region. Twelve-Factor's advice is blunt: keep the same type and version of each backing service across environments to close this gap.1
The config trap: the most common cause
If the app breaks in production immediately, before any load or data issue could bite, the cause is almost always configuration. Config is everything that changes between environments: database URLs, API keys, secrets, feature flags, host and CORS settings. It lives outside your code, so code that is completely correct can still break purely because production is set up differently.
Twelve-Factor's rule is to store config in the environment, strictly separated from code, so the same build runs anywhere and only its settings change.2 The classic failures are a missing environment variable that was set on your laptop but not on the server, a secret that is empty in production, or a URL still pointing at localhost. When "works locally" breaks the instant it deploys, check config first: it is the highest-probability culprit and the fastest to rule out.
You cannot reproduce it, so watch it
Here is the uncomfortable truth: some production breaks you cannot reproduce locally, by definition. You cannot put a million real rows, a thousand concurrent users, and a flaky third-party API on your laptop. Trying to reproduce a prod-only bug in development is often a dead end, and hours of it is how a small issue becomes a long outage.
So you flip the approach. Instead of reproducing the break, you watch production directly. That means capturing logs, traces, and error and success rates from the live app, the same latency, traffic, errors, and saturation that make up the golden signals.3 With those in place, a break you could never reproduce is fully visible: you read the trace of a failing production request and see exactly where it broke.4 The bug that "only happens in production" is not a mystery, it is just a bug you have to observe where it actually happens.
Catching prod-only breaks with Fixter
Fixter is built for exactly the break you cannot reproduce. You drop in the SDK, built on OpenTelemetry, and your production logs, traces, and metrics are stored. When something breaks in production, whether it throws or just quietly does the wrong thing, you ask about it in plain language from your editor, and Fixter runs the first pass of the investigation: it finds the failing requests, reads their traces, and tells you where it broke and since when.
And your code never leaves your machine to do it. Fixter holds the production signal, not your source, so you see what production is doing and make the fix locally. That is the honest workflow for prod-only bugs: observe in production, fix in your editor.
Shrink the gap where you can, and watch production for the rest. For the specific prod-only cases, see why is my app slow and why is my database query slow.
Key takeaways
- Local is one user, clean data, and no real network; production is the opposite, and that gap is where bugs live
- The usual culprits are data volume, concurrency, real external services, messy data, and different infrastructure
- An immediate break after deploy is almost always config: a missing env var, secret, or wrong URL
- Some prod-only bugs cannot be reproduced locally, so watch production instead of trying
- Fixter captures production telemetry and finds the failing trace, so you fix it in your editor
Frequently asked questions
Why does my app work locally but break in production?
Because production differs from your laptop in the ways that matter: it has real data volume, real concurrent users, different configuration and secrets, and real external services with their own latency and limits. Local hides all of that behind a tiny, clean, single-user setup, so a bug that only appears under those conditions never shows up until you deploy.
What is the most common reason code works locally but not in production?
Configuration. A missing or different environment variable, a database URL pointing somewhere else, an unset secret, or a host and CORS setting that differs between environments. Config lives outside your code and changes per environment, so code that is correct can still break purely because production is configured differently.
How do I debug an issue that only happens in production?
You often cannot reproduce it locally, so you watch production instead. Capture logs, traces, and error and success rates from the live app, then read the trace of a failing request to see exactly where it broke. The break you cannot reproduce is still fully visible in production telemetry.
Why is my app slow in production but fast locally?
Almost always data volume and concurrency. A query with no index is instant on the hundred rows you have locally and slow on the million in production, and code that is fine for one user can fall over when many hit it at once. Tracing a slow production request shows which of these it is.
How do I stop production surprises?
Shrink the gap where you can, keep development and production on the same type and version of backing services and the same config shape, and accept that some differences (real data, real load, real third parties) cannot be reproduced. For those, the answer is production monitoring so you see the break the moment it happens.
Sources
- The Twelve-Factor App, Dev/prod parity (the time, personnel, and tools gaps; keep the same backing services across environments)
- The Twelve-Factor App, Config (store configuration in the environment, separate from code)
- Google, Site Reliability Engineering, the four golden signals (latency, traffic, errors, saturation)
- OpenTelemetry, Traces (a trace shows where a request went and where it failed)