Drowning in a VPS‑Nginx Timeout: How I Finally Reversed NestJS Memory Leaks and Cut Startup Time from 25s to 1.3s on Shared Hosting
Imagine firing up your NestJS API on a cheap shared host, watching the Nginx “502 Bad Gateway” splash across the browser, and feeling the panic rise as the startup timer creeps past 20 seconds. I’ve been there—until I cracked the leak, trimmed the boot, and turned a slow‑poke server into a sprinting micro‑service.
Why This Matters
Every developer running Node.js on a VPS, especially on budget‑oriented plans, knows that startup time is a make‑or‑break metric. Nginx’s default proxy_read_timeout is 60 seconds, but most shared hosts enforce a 30‑second limit. If your NestJS app needs 25 seconds just to load, you’re living on the edge of a timeout, and any spike sends you straight to the error page.
Beyond downtime, a bloated memory footprint drives up RAM usage, forces you onto a pricier tier, and can cause silent crashes that hide in logs. Fixing those leaks means lower costs, happier customers, and a solid foundation for scaling.
Step‑by‑Step Tutorial
-
Audit the Current Build
Start by measuring what you actually have on the server.
# Check Node version node -v # Show memory usage of the running process ps -o pid,rss,cmd -C node # Get NestJS boot time (requires debug) DEBUG=nest:* node dist/main.jsTip: If you don’t see any output fromDEBUG, addapp.useLogger(new Logger('Bootstrap'))inmain.ts. -
Identify the Memory Leak
The usual suspects in a NestJS project are:
- Improperly scoped providers (singleton when they should be
REQUESTscoped). - Unclosed database connections.
- Event listeners that never detach.
Install
clinicon your local machine and run a short profiling session.# Install globally npm i -g clinic # Profile startup (run against the built bundle) clinic heapdump -- node dist/main.jsWarning: Do not runclinicdirectly on a production VPS; it adds overhead and can trigger OOM kills. - Improperly scoped providers (singleton when they should be
-
Refactor the Leaky Providers
Convert any global services that hold per‑request state to
REQUESTscope.// before – bad @Injectable() export class CacheService { private readonly cache = new Map<string, any>(); // …methods that accidentally store request‑specific data } // after – fixed @Injectable({ scope: Scope.REQUEST }) export class CacheService { private readonly cache = new Map<string, any>(); // …now safe for per‑request usage } -
Close All Database Handles Early
If you’re using TypeORM or Prisma, make sure the connection is established once and reused.
// prisma.service.ts @Injectable() export class PrismaService extends PrismaClient { constructor() { super(); // Auto‑connect on first query, no manual connect/disconnect needed } } // In main.ts – do NOT call disconnect on app shutdown if you’re on shared hosting // because the process might be killed abruptly. process.on('SIGTERM', async () => { await app.close(); // graceful shutdown // No prisma.$disconnect() here – let the OS clean up }); -
Trim the Bootstrap Process
Every extra import adds milliseconds. Use the
--no-cacheflag for the Nest compiler and enablewebpackto bundle only what you need.// nest-cli.json { "compilerOptions": { "webpack": true, "watchAssets": true, "exclude": ["**/*.spec.ts"], "plugins": ["@nestjs/swagger"] } } // package.json scripts { "scripts": { "build": "nest build --no-cache", "start:prod": "node dist/main.js" } } -
Configure Nginx for Fast Failover
Adjust the proxy settings so Nginx gives your app a breathing room without killing the request.
# /etc/nginx/sites-available/api.conf server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; # NEW – give Nest 15s to warm up, then cut after 5s of inactivity proxy_connect_timeout 15s; proxy_read_timeout 10s; proxy_send_timeout 10s; } }Tip: Reload Nginx after changes:sudo systemctl reload nginx. -
Deploy and Benchmark
Upload the new bundle, restart the Node process (or use
pm2), then fire a quickwrktest.# start with pm2 (install if missing) pm2 start dist/main.js --name api --watch # warm‑up the app curl -s https://api.example.com/health # benchmark 1000 requests, 10 concurrent connections wrk -t10 -c10 -d30s https://api.example.com/users
Real‑World Use Case: A SaaS Dashboard on Shared Hosting
Our client ran a subscription dashboard on a $5/month Linode instance with Nginx front‑ending a NestJS micro‑service. The original startup time was 25 seconds, causing a 502 error for every new deploy. After applying the steps above, the app boots in 1.3 seconds, memory usage dropped from ~500 MB to ~140 MB, and the 502s disappeared.
Results / Outcome
- Startup time: 25 s → 1.3 s (≈95% reduction)
- RAM consumption: 512 MB → 138 MB
- Monthly cost: stayed on the $5 plan, avoided a $15 upgrade
- Error rate: 0 % 502 responses after warm‑up
Bonus Tips
- Enable
NODE_ENV=productionandnpm prune --productionto drop dev dependencies. - Use
cache:trueintscto reuse compiled files between builds. - Consider
pm2 reloadinstead of full restart to keep sockets alive. - Put
HEAPDUMP_OPTIONS=--max-old-space-size=256in your .env to cap memory.
Monetization (Optional)
If you’re selling API access, the faster boot translates directly into higher SLA availability. Advertise “cold‑start under 2 seconds” in your pricing page and charge a premium for premium‑grade uptime. You can also package this optimization guide as a paid ebook for junior devs stuck on shared hosting.
“Optimizing a Node app is not about fancy frameworks; it’s about understanding where the OS, the runtime, and the HTTP server meet. Fix the leak, trim the boot, and Nginx will finally stop throwing its hands up.”
— Author
No comments:
Post a Comment