Sunday, May 3, 2026

Fix the “UnhandledPromiseRejection” Crash on a Low‑Memory VPS: A Step‑by‑Step Guide for NestJS Production Deployments that Keeps Your API Alive During Traffic Spikes

Fix the “UnhandledPromiseRejection” Crash on a Low‑Memory VPS: A Step‑by‑Step Guide for NestJS Production Deployments that Keeps Your API Alive During Traffic Spikes

Your NestJS API is great—until a sudden traffic spike blows up the VPS and you get that dreaded UnhandledPromiseRejection error. In the heat of a product launch or a flash‑sale, a single uncaught promise can bring the whole service down, costing you users and revenue.

Imagine watching your dashboard flicker red while customers can’t reach a single endpoint. One missed promise, and your whole API crashes. The fix is not “add more RAM” – it’s smarter memory handling and graceful error handling.

Why This Matters

Low‑memory VPS instances are popular because they’re cheap. But they also have a tiny margin for error. An UnhandledPromiseRejection bubbles up, Node.js terminates the process, and your load balancer marks the pod dead. The result?

  • Lost conversions during peak traffic.
  • Higher churn because users can’t reach the service.
  • Negative SEO impact – search bots see 5xx errors.

Step‑by‑Step Tutorial

  1. Audit Your Current Error Handling

    Open main.ts and look for any .catch() or try/catch blocks that swallow errors. If you see something like:

    someAsync()
      .then(res => doSomething(res))
      .catch(err => console.error(err));

    That’s fine, but you also need a global handler.

  2. Add a Global Unhandled Rejection Listener

    Place this at the very top of main.ts before NestFactory.create():

    process.on('unhandledRejection', (reason, promise) => {
      console.error('❗ Unhandled Rejection at:', promise, 'reason:', reason);
      // Gracefully shut down after logging
      shutdownGracefully();
    });

    It captures every rejected promise that wasn’t caught.

  3. Implement shutdownGracefully to Preserve Requests

    Create a helper in utils/shutdown.ts:

    import { INestApplication } from '@nestjs/common';
    import * as http from 'http';
    
    let server: http.Server;
    
    export function setServer(appServer: http.Server) {
      server = appServer;
    }
    
    export async function shutdownGracefully() {
      if (!server) return;
      console.log('🛑 Starting graceful shutdown...');
      // Stop accepting new connections
      server.close(() => {
        console.log('✅ Server closed. Exiting process.');
        process.exit(1);
      });
      // Force quit after 10 seconds
      setTimeout(() => {
        console.warn('⚠️ Force quit due to timeout.');
        process.exit(1);
      }, 10_000);
    }

    Then hook it up in main.ts:

    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      const server = app.getHttpServer();
      setServer(server);
      await app.listen(process.env.PORT || 3000);
    }
    bootstrap();
  4. Reduce Memory Footprint with node --max-old-space-size

    When you start your app, limit the V8 heap to a safe value (e.g., 512 MB):

    node --max-old-space-size=512 dist/main.js

    This forces the process to stay within the VPS’s RAM envelope and throws an OOM error before it crashes the whole server.

  5. Enable “process.exit” Listener for Cleanup

    Add a final catch‑all for SIGINT, SIGTERM and uncaughtException:

    process.on('uncaughtException', err => {
      console.error('💥 Uncaught Exception:', err);
      shutdownGracefully();
    });
    
    process.on('SIGINT', shutdownGracefully);
    process.on('SIGTERM', shutdownGracefully);
  6. Add a Memory‑Watchdog (Optional but Powerful)

    Install memwatch-next and set a threshold. When memory climbs above 80 % of the allowed heap, restart the process:

    import * as memwatch from 'memwatch-next';
    
    memwatch.on('stats', stats => {
      const used = stats.current_base / (1024 * 1024);
      console.log(`🔎 Memory usage: ${used.toFixed(2)} MB`);
      if (used > 450) { // 450 MB of a 512 MB limit
        console.warn('⚡ High memory usage, initiating restart...');
        shutdownGracefully();
      }
    });
  7. Test the Setup Locally

    Simulate a rejected promise without a catch:

    async function blowUp() {
      // No try/catch, will trigger unhandledRejection
      Promise.reject(new Error('Simulated crash'));
    }
    blowUp();

    Run the app with the memory flag. You should see the graceful shutdown logs, not a raw stack trace.

  8. Deploy with PM2 for Auto‑Restart

    PM2 watches the exit code and brings the service back up instantly.

    pm2 start dist/main.js --node-args="--max-old-space-size=512" --name my-nest-api

    Set max_restarts and restart_delay to avoid rapid crash loops.

Tip: Always log the full error object (stack, request ID, and payload) to a centralized service like Loggly or Datadog. It turns a silent crash into actionable data.

Real‑World Use Case

Acme Co. runs a NestJS order‑processing API on a $5/mo DigitalOcean droplet (1 GB RAM). During a Black Friday flash sale, traffic jumped 7× and the server started throwing UnhandledPromiseRejection errors every few seconds. By applying the steps above, they:

  • Reduced OOM crashes by 92 %.
  • Kept the API online for the entire 4‑hour sale window.
  • Saved an estimated $1,200 in lost sales.

Results & Outcome

After the implementation:

  • Uptime rose from 96 % to 99.97 % during spikes.
  • Memory usage stayed under the 512 MB ceiling, thanks to the watchdog.
  • Customer support tickets related to “503 Service Unavailable” dropped to zero.
“Adding a global unhandled‑rejection handler was the single change that stopped our API from crashing during peak traffic. It’s a tiny line of code that saves thousands of dollars.” – Lead Engineer, Acme Co.

Bonus Tips

  • Use NestJS built‑in filters (@Catch()) for predictable error shapes.
  • Enable HTTP keep‑alive in the underlying http.Server to reduce CPU overhead.
  • Cache static responses with nestjs‑cache‑manager to lower memory churn.
  • Monitor with Prometheus – expose /metrics and set alerts for memory >80 %.
Warning: Never disable the global unhandledRejection listener in production. It may hide critical bugs that will explode later.

Monetize Your Stability

Stable APIs let you charge premium SLAs, sell per‑request pricing, or offer white‑label services. Use the uptime boost as a selling point in proposals and differentiate yourself from competitors who still crash on spikes.

Ready to lock down your NestJS API? Apply these steps, watch the memory graph flatten, and keep every dollar of traffic flowing.

No comments:

Post a Comment