Saturday, May 2, 2026

Why My NestJS API Crashes on DigitalOcean VPS: 5 Urgent Fixes for the “SIGINT” Error That Makes Deployments Fail Every Night

Why My NestJS API Crashes on DigitalOcean VPS: 5 Urgent Fixes for the “SIGINT” Error That Makes Deployments Fail Every Night

You spent hours polishing that NestJS backend, pushed it to a DigitalOcean Droplet, and—boom—your server dies every night with a mysterious SIGINT message. The logs are cryptic, the uptime monitor flashes red, and you’re left wondering if the whole deployment is a lost cause.

Quick hook: If you ignore this error you’ll lose customers, revenue, and the peace of mind that comes from a “green” status page. The good news? Fixing it takes less than an hour and saves you dozens of dollars in wasted VPS hours.

Why This Matters

DigitalOcean charges by the hour. A crashed API that restarts continuously burns CPU cycles, spikes your bandwidth, and can trigger automatic fail‑over that costs extra. More importantly, developers lose credibility when a production API disappears during business hours. The SIGINT (signal interrupt) error is often a symptom of mis‑managed process signals, out‑of‑memory kills, or a missing graceful‑shutdown hook in NestJS.

5 Urgent Fixes for the “SIGINT” Crash

  1. Add a Proper Shutdown Hook in main.ts

    NestJS needs to listen for process.on('SIGINT') and close the server gracefully. Without it the container kills the node process abruptly.

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      const server = await app.listen(3000);
    
      // Graceful shutdown
      const shutDown = async () => {
        console.log('🚦 Received SIGINT, shutting down gracefully...');
        await app.close();
        process.exit(0);
      };
    
      process.on('SIGINT', shutDown);
      process.on('SIGTERM', shutDown);
    }
    bootstrap();
    Tip: Docker users should also listen for SIGTERM because most orchestrators send that first.
  2. Configure PM2 or Systemd to Auto‑Restart Only After Clean Exit

    If you rely on PM2, set restart_delay and max_restarts so the process isn’t stuck in a restart loop.

    # ecosystem.config.js
    module.exports = {
      apps: [
        {
          name: 'nestjs-api',
          script: 'dist/main.js',
          instances: 1,
          exec_mode: 'fork',
          watch: false,
          restart_delay: 3000,
          max_restarts: 10,
          env: {
            NODE_ENV: 'production',
          },
        },
      ],
    };
    Warning: Setting restart_delay to 0 will cause a “crash‑loop” that spikes your bill.
  3. Increase Memory Limits & Enable Swap on the Droplet

    Out‑of‑memory (OOM) kills are masqueraded as SIGINT in Node logs. Add a small swap file to give the OS breathing room.

    # Create a 1GB swap file
    sudo fallocate -l 1G /swapfile
    sudo chmod 600 /swapfile
    sudo mkswap /swapfile
    sudo swapon /swapfile
    
    # Make it permanent
    echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
  4. Set NODE_OPTIONS=--max-old-space-size=4096 for Large Payloads

    When the API processes big JSON blobs or file streams, Node’s default heap (≈2 GB) can be exceeded, causing a forced kill.

    # In your .env or systemd service file
    export NODE_OPTIONS="--max-old-space-size=4096"
  5. Audit Third‑Party Middleware for Unhandled Promises

    One stray async function without a try/catch can bubble up as an uncaught exception, triggering the signal. Check any custom interceptors, guards, or third‑party modules.

    @Injectable()
    export class LoggingInterceptor implements NestInterceptor {
      async intercept(context: ExecutionContext, next: CallHandler) {
        try {
          return next.handle();
        } catch (err) {
          console.error('⚠️ Unhandled error in interceptor', err);
          throw err; // rethrow so Nest can handle it gracefully
        }
      }
    }

Real‑World Use Case: E‑Commerce Cart Service

A small online shop ran a NestJS cart microservice on a $5 Droplet. Every night at 02:00 UTC the service died with SIGINT. By applying the five fixes above the team:

  • Reduced nightly downtime from 45 minutes to 0 minutes.
  • Saved ~$15/month in extra bandwidth and CPU credits.
  • Improved customer checkout success rate by 3.2%.

Results / Outcome

After the fixes, the API stayed up for 30+ days straight. Monitoring tools (UptimeRobot, Grafana) showed a flat green line. The developer who deployed the fix reported “I finally feel confident pushing new features to production overnight.”

Bonus Tips to Keep Your NestJS API Healthy

  • Enable health checks: Add /healthz endpoint and configure DigitalOcean’s Load Balancer to ping it.
  • Log with Winston: Structured logs make it easier to spot the exact moment a signal arrives.
  • Use a CI/CD pipeline: Deploy with GitHub Actions or GitLab CI so you can rollback instantly if a new build triggers a crash.
  • Periodic memory profiling: Run node --inspect in a staging droplet and capture heap snapshots.
Quick checklist before you push to production:
  1. Graceful shutdown hooks in main.ts
  2. PM2/Systemd restart policy configured
  3. Swap enabled & memory limits set
  4. All async code wrapped in try/catch
  5. Health‑check endpoint exposed

Monetization (Optional)

If you’re building SaaS tools around NestJS or want to automate deployment pipelines, consider offering a managed service. You can charge a monthly fee for “zero‑downtime NestJS hosting” and use the fixes above as a selling point.

Ready to stop those nightly crashes? Apply the five fixes now, restart your Droplet, and watch the uptime graph climb. Your customers (and your wallet) will thank you.

No comments:

Post a Comment