A Node.js app that crashes or keeps restarting in production is usually caused by one of five things: unhandled promise rejections, memory leaks, a blocked event loop, memory limits that don't match the runtime, or differences between local and production environments. The exit code is the fastest clue: 1 points to an uncaught error, 134 to heap exhaustion, and 137 to the process being killed for exceeding its memory limit.
Typical symptoms | Silent exits, repeated PM2 or container restarts, heap out of memory errors, crashes under traffic |
Most common causes | Unhandled rejections, memory leaks, event loop blocking |
How we fix it | Recover the stack trace, profile memory and event loop, reproduce under load, fix the root cause, add monitoring |
Turnaround | Same-day diagnosis; most fixes in 24–48 hours |
Price | Live Debug from $20; fixed-price quote for the full fix |
Signs Your Node.js App Has a Production Crash Problem
The process exits with no useful error in your logs
PM2, Docker, or Kubernetes keeps restarting the app
You see FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
The container stops with exit code 137 (killed for exceeding memory)
The app crashes only under traffic spikes, never in testing
Health checks fail and the load balancer marks instances unhealthy
Memory usage climbs steadily until the next restart
Why Node.js Apps Crash in Production
Node.js runs your JavaScript on a single main thread. That design is fast, but it means one unhandled error or one blocked operation affects every user at once. Production also exposes conditions local testing rarely does: sustained traffic, real data volumes, slow third-party APIs, and memory limits set by your container or host.
Most production crashes fall into five causes.
1. Unhandled promise rejections and uncaught exceptions
Since Node.js 15, an unhandled promise rejection terminates the process by default. A single await without error handling around a failing database call or API request can take the whole server down. A throw inside a callback or event emitter with no handler does the same.
2. Memory leaks
Objects that are never released pile up in the heap: growing in-memory caches, event listeners added on every request, closures holding large objects, or global arrays used as queues. Memory rises slowly until V8 hits its heap limit or the container is killed.
3. Blocking the event loop
Synchronous work such as large JSON.parse calls, heavy loops, synchronous file reads, or CPU-bound tasks like image processing freezes the event loop. Requests pile up, health checks time out, and your orchestrator restarts the "unresponsive" app, which looks like a crash.
4. Memory limits that don't match the runtime
The container may allow 512 MB while Node's heap is allowed to grow past it, or the heap limit is set far below what the workload needs. Either way, the process is killed without a JavaScript stack trace.
5. Environment differences
Missing environment variables, a different Node.js version, unpinned dependencies, or connection limits on production databases cause failures that never appear locally.
How We Diagnose the Crash
Read the exit signal. Exit code 1 points to an uncaught error; 134 usually means V8 aborted on heap exhaustion; 137 means the process was killed externally, usually by the container's memory limit.
Recover the missing stack trace. Add structured logging plus temporary uncaughtException and unhandledRejection handlers that record the error before exit.
Profile memory over time. Capture heap snapshots at intervals, or use --heapsnapshot-near-heap-limit, and compare them to find objects that keep growing.
Measure event loop delay. Track lag with perf_hooks.monitorEventLoopDelay() or Clinic.js to find synchronous code blocking requests.
Reproduce under load. Replay realistic traffic with a load-testing tool to trigger the crash on demand.
Compare environments. Check Node.js version, dependency lockfile, environment variables, and container memory settings against local.
How We Fix It
Root cause | Fix |
Unhandled rejections | Add error handling at every async boundary, centralized Express/Fastify error middleware, and alerting on rejections |
Memory leaks | Remove the retaining references, cap or replace in-memory caches (e.g. LRU or Redis), and clean up listeners and timers |
Blocked event loop | Move CPU-heavy work to worker threads or a job queue, stream large payloads, and replace sync I/O |
Mismatched memory limits | Align --max-old-space-size with container limits and set process manager restart thresholds as a safety net |
Environment drift | Pin the Node.js version, commit the lockfile, and validate required environment variables at startup |
Every fix also includes graceful shutdown so deploys and restarts stop dropping in-flight requests: handle SIGTERM, stop accepting connections, finish active requests, then close database connections.
We test the fix under the same load that caused the crash before handing it back.
Example Fix
Situation: A SaaS API on Node.js and Express restarted every 4–6 hours in Docker, with no error in the logs.
Cause: An in-memory object used to cache user sessions was never cleared, and the container was killing the process at its memory limit (exit code 137).
Fix: Moved sessions to Redis with expiry, set the heap limit below the container limit, and added memory alerts.
Result: Memory stayed flat under load, and the unplanned restarts stopped.
How to Keep It From Happening Again
Monitor memory and event loop lag, not just CPU, and alert on steady growth.
Treat every await as a failure point; lint for floating promises in CI.
Load test before major releases so production traffic isn't your first test.
What You Get
Root cause confirmed and explained in plain language
Tested fix delivered as a pull request or patch
Monitoring or alerting added for the specific failure
Short prevention notes for your team
Frequently Asked Questions
Why does my Node.js app crash in production but not locally? Production adds real traffic, larger data, slower external services, and strict memory limits. Leaks and event loop blocking often take hours of real load to surface, which local testing rarely reproduces.
Should I just restart the app automatically with PM2? Auto-restart keeps the app available, but it hides the problem. Users still get dropped requests during each crash, and leaks usually get worse as traffic grows. Use restarts as a safety net while the root cause gets fixed.
What does exit code 137 mean for a Node.js container? The process was killed by a SIGKILL signal, most often because the container exceeded its memory limit. That's why no JavaScript error appears in your logs.
Will increasing --max-old-space-size fix the crash? Only if the app genuinely needs more memory. If there's a leak, a bigger heap just delays the crash. We confirm which case you have before changing limits.
Do you need access to our production servers? Not at the start. Logs, crash output, and the relevant code are usually enough to diagnose. We request limited access only if we need to profile the live environment.
Related Problems
Python app memory leak
MySQL too many connections error
Docker container exits immediately
Kubernetes pod CrashLoopBackOff
Production down right now? → Production Support
Crashes only at scale? → Application Scalability Optimization
Stop the Restarts
Send the crash log or exit code. Get a diagnosis and a fixed price.