For months, TicketingSailor's /health endpoint told ECS everything was fine. It genuinely believed that. The database had been unreachable for minutes before anyone found out, and the health check had no idea.

TicketingSailor's Shape

The shape is the standard one: NestJS on ECS/Fargate, RDS Postgres, an ALB in front running target-group health checks against each task. NestJS scaffolds a health controller in about five minutes, and that's roughly what I shipped. It pinged the process, got a response, returned 200, confirming only that the Nest process itself was running.

Why I Skipped Terminus

When I built the health check, there were two real options. Reach for @nestjs/terminus, which ships structured health indicators for databases, disk, and memory. Or hand-roll a controller that confirms the Nest process is alive and responding to HTTP, nothing more.

I hand-rolled it. Terminus felt like an extra dependency for a problem that seemed simple: is the app up, yes or no. Five lines returning { status: 'ok' } took less time than reading Terminus's docs, and for a solo project moving fast, that felt like the right tradeoff at the time.

Here's where it went wrong. The database connection pool got exhausted during a traffic spike, and every real request touching Postgres started erroring out with a 500. The /health endpoint never touched Postgres at all, so it kept confirming the HTTP server was listening, which it was. ECS kept routing traffic into a container that was, from the caller's side, completely broken.

Nobody internal caught it. A customer did, reporting failed requests before any alarm, dashboard, or health check said a thing was wrong. Once it was traced back to the pool, the bad window closed in minutes, not hours. Minutes is still a customer finding out your system is broken before you do.

That's the actual trap with a hand-rolled check. It answers a real question, just not the one production needs answered. Liveness (is the process alive) and readiness (can this instance serve a request right now) are different questions, and a check that only answers the first will happily keep a broken container in rotation, confident the whole time that it's doing its job correctly.

What Terminus Actually Buys You

Setting this up again, I'd reach for Terminus from day one and wire in an actual database indicator, not just the process check. Terminus's database health indicators run a real query as part of the health response, effectively a SELECT 1 against Postgres. That one addition, a real query against Postgres on every check, would have caught the exact failure that took down TicketingSailor.

There's a second piece I'd add alongside it. Terminus gives you a correct signal. Whether ECS actually acts on that signal is a separate setting, the ALB's target-group deregistration threshold, and it's easy to assume the first half was the whole job. Worth checking too: the health check's own interval and unhealthy-threshold count. Tuned loosely, a real failure can take several missed checks in a row before ECS stops routing to the broken task, which stretches the exact window a customer notices before you do.

The Bigger Gap Nobody Internal Caught

Fixing the health check is the smaller fix. The bigger one sits further back: nothing internal caught this, a customer did. A better check closes this one specific gap. It doesn't change the fact that our monitoring's first line of defense, that day, was a support ticket.

If you're standing up a NestJS service on ECS today, fix the check, but don't let it stand in for real alerting. I'd go further: a health check that only gets exercised by your own orchestrator is doing half a job. If a customer can still notice a failure before any internal system does, pointing at Terminus as "the fix" lets the actual gap, no alert on connection-pool exhaustion or rising error rate, go unaddressed.

Keep Reading