Sunday, May 3, 2026

How I Tracked Down the One‑Minute Timeout “Connection Refused” Bug When Deploying a NestJS API to a Cheap VPS with Shared Node Version Control 🚑

How I Tracked Down the One‑Minute Timeout “Connection Refused” Bug When Deploying a NestJS API to a Cheap VPS with Shared Node Version Control 🚑

Hook: I was ready to launch my MVP, but after one minute the API crashed with “Connection Refused”. The whole thing happened on a $5/month VPS that shared a Node version with dozens of other users. What went wrong? Spoiler: It was a silent timeout caused by a mismatched Node version and a stray firewall rule. This post shows you exactly how I found the culprit in 30 minutes and how you can avoid the same nightmare.

Why This Matters

If you’re building a SaaS, a side‑hustle, or a freelance automation tool, you often choose the cheapest VPS to keep costs low. Those servers usually run nvm or shared /usr/bin/node binaries. A tiny version mismatch can turn a perfectly fine NestJS service into a “connection refused” ghost that disappears after 60 seconds. The cost? Hours of debugging, angry customers, and a missed launch window.

Step‑by‑Step Tutorial

  1. Reproduce the Error Locally

    Run the same Docker image you plan to push to the VPS. If the API works locally but not on the server, the problem is environmental.

    Tip: Use docker-compose up with the same env file you’ll use on the VPS.

  2. Check the Node Version on the VPS

    ssh user@cheap-vps
    node -v   # <-- what you get

    If the output is v12.x but your NestJS project requires v18, you’ve already found the first red flag.

  3. Inspect the Global npm Packages

    npm list -g --depth=0

    Shared servers often have a globally installed pm2 or nestjs-cli that was compiled against an older Node version, causing silent crashes.

  4. Verify Firewall & SELinux Settings

    Even if the port (e.g., 3000) is open, a per‑process timeout can be enforced by iptables “connection‑track” rules.

    sudo iptables -L -n -v | grep 3000
  5. Enable Detailed Logging in NestJS

    Add a logger middleware that writes to stdout and a file. This catches the moment the process dies.

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import * as fs from 'fs';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule, {
        logger: ['error', 'warn', 'log', 'debug', 'verbose'],
      });
      const logStream = fs.createWriteStream('app.log', { flags: 'a' });
      app.useLogger(app.getLogger());
      process.stdout.write = process.stderr.write = logStream.write.bind(logStream);
      await app.listen(3000);
    }
    bootstrap();
  6. Deploy with the Correct Node Version

    Install nvm (if allowed) and force the exact version your package.json specifies.

    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
    source ~/.bashrc
    nvm install 18.20.0
    nvm use 18.20.0
    node -v   # should print v18.20.0
  7. Restart the Service with pm2 and Watch the Logs

    pm2 start dist/main.js --name nest-api
    pm2 logs nest-api --lines 100

Warning: Do not skip the nvm install step on a shared VPS. The default node binary is often an ancient LTS that lacks support for newer ES modules used by NestJS.

Real‑World Use Case

My client needed a quick URL‑shortener API. We built it with NestJS, TypeORM, and a PostgreSQL instance on the same VPS. The app ran perfectly on my Mac, but the first request from the client’s frontend hit a “Connection Refused” after exactly 60 seconds. After following the steps above, we discovered:

  • The VPS was running node v12.22.1 (the default for the provider).
  • Our package.json declared "engines": {"node": ">=18"}, so npm install succeeded but the runtime threw a silent ERR_REQUIRE_ESM after the first async call.
  • Once we switched to nvm and installed Node 18, the API responded in 45 ms instead of timing out.

Results / Outcome

By fixing the Node version and clearing the stray firewall rule, the API stayed alive indefinitely. Here’s a quick snapshot of the performance before and after:

# Before fix (timeout after 60s)
GET /shorten → connection refused (ERR_CONNECTION_REFUSED)

# After fix
GET /shorten → 200 OK, 41ms response time
Server uptime: 7 days 23 hrs (no restarts)

Result: Zero downtime, 10× faster response, and the client could finally launch their marketing campaign on schedule.

Bonus Tips

  • Lock Node version in .nvmrc: echo "18.20.0" > .nvmrc and add nvm use to your deployment script.
  • Use a health‑check endpoint: Add /healthz that returns 200 only when DB and external services are reachable.
  • Enable systemd watchdog: Prevent silent kills by configuring WatchdogSec=30 in the service file.
  • Monitor with pm2 status: Set up an email alert for status !== online.
  • Auto‑restart on version mismatch: Add a pre‑start script that checks node -v against package.json and aborts if they differ.

Monetize This Knowledge

If you’re tired of chasing “one‑minute timeout” ghosts, I’ve compiled a 30‑page cheat sheet with exact VPS setup commands for the top 5 cheapest providers. Grab it now for $9 and stop losing hours to environment headaches.

© 2026 Your Blog Name – All rights reserved.

No comments:

Post a Comment