NestJS Health Check Failing on Render — What's Actually Different

NestJS health check failing on Render almost never means what it sounds like. The app boots, logs look clean, requests work fine if you curl the service directly — and Render still marks it unhealthy and cycles the container. The gap is almost always in the health check itself: the wrong path, a timeout that's too aggressive, or a route accidentally guarded by the same auth middleware protecting the rest of your API.
Short answer: check three things in this order — does the path you configured in Render's dashboard actually match a route your NestJS app exposes, does that route respond well within Render's timeout window, and is that route excluded from any global guards or interceptors that might be silently returning a 401 or 500 instead of a 200. Fix whichever of those three is wrong, and the "unhealthy" status disappears without you touching your actual application logic.

Why Render's Health Check Reports Unhealthy for a Perfectly Fine App
Render doesn't inspect your process, your memory usage, or your logs to decide if a service is healthy. It probes one specific HTTP path, on a schedule, and expects a fast 200 back. Everything Render "knows" about your app's health comes from that single signal. If the path is wrong, the response is slow, or the route returns anything other than a clean success status, Render behaves exactly as designed — it assumes the service is down and restarts it, even if your NestJS process has been happily serving other routes the entire time.
This is worth internalizing because it reframes the debugging: you're not chasing an application bug. You're auditing a single, narrow contract between your app and Render's probe, and the fix lives almost entirely in that contract, not in your business logic.
Building the Health Route Correctly With @nestjs/terminus
The official @nestjs/terminus package exists specifically for this, and it's worth using over a bare { status: 'ok' } route because it gives you a structured way to check real dependencies without accidentally making the endpoint itself fragile:
1// health.controller.ts
2import { Controller, Get } from '@nestjs/common';
3import { HealthCheckService, HttpHealthIndicator, HealthCheck } from '@nestjs/terminus';
4
5@Controller('health')
6export class HealthController {
7 constructor(
8 private health: HealthCheckService,
9 private http: HttpHealthIndicator,
10 ) {}
11
12 @Get()
13 @HealthCheck()
14 check() {
15 return this.health.check([
16 // A dependency check should still resolve fast — don't chain
17 // a slow downstream call here, or you've just moved the problem.
18 () => this.http.pingCheck('self', 'http://localhost:3000'),
19 ]);
20 }
21}Terminus's own repository has indicator modules for database connections, disk, and memory if you want the health route to mean something beyond "the process didn't crash" — just keep every check inside it fast, since a slow health route fails Render's probe for a completely different reason than an actually broken app.

The Trap Almost Nobody Expects: A Global Auth Guard Eating the Health Route
Here's the one that costs people the most debugging time. If your AppModule applies an AuthGuard globally — a common, otherwise-correct pattern — it applies to every route by default, including /health. Render's probe doesn't carry a bearer token. It gets a 401, not a 200, and the platform does exactly what it's supposed to do with a failing check: restart a service that was never actually broken.
1// app.module.ts
2import { Module } from '@nestjs/common';
3import { APP_GUARD } from '@nestjs/core';
4import { AuthGuard } from './auth/auth.guard';
5
6@Module({
7 providers: [
8 {
9 provide: APP_GUARD,
10 useClass: AuthGuard,
11 },
12 ],
13})
14export class AppModule {}(If you've ever added @Public() decorators to half your controllers just to "make the errors go away" without figuring out why they were 401-ing in the first place — the health route is very often the one nobody thought to check, because nobody's manually testing it the way they test a login flow.)
The fix is explicit exclusion, not a global bypass:
1// auth.guard.ts
2import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
3
4@Injectable()
5export class AuthGuard implements CanActivate {
6 canActivate(context: ExecutionContext): boolean {
7 const request = context.switchToHttp().getRequest();
8 if (request.path === '/health') return true;
9 // ...your real auth logic
10 }
11}
Configuring Render's Health Check Path Correctly
Render's own documentation confirms the mechanism directly: in the dashboard, under your service's settings, the health check path is a plain text field — and it defaults to nothing meaningful if you skip it. Set it to whatever route your HealthController actually exposes (/health in the example above), and make sure there's no trailing-slash or base-path mismatch between what's configured there and what your NestJS app is actually serving under a global prefix, if you've set one with app.setGlobalPrefix().
The Opinion Part: A Health Check's Job Is to Answer Fast, Not to Prove Perfection
Here's the position worth stating plainly: a health check endpoint that Render (or Railway, or Kubernetes) restarts your service based on should do one job — confirm the process is alive and can respond — and nothing more. Chaining a full dependency audit (database, third-party APIs, disk) into the same route that gates automated restarts is how a slow downstream API turns into your entire service getting cycled every few minutes for a problem that has nothing to do with whether your app itself is fine. Keep the liveness check nearly instant and dependency-free. Put the deeper "is everything actually healthy" check on a separate route for human dashboards, not automated restart decisions.
This distinction matters more than it looks like it should, because a misconfigured health check causing restart loops is genuinely self-inflicted downtime — and for over 90% of mid-size and large enterprises, an hour of downtime now costs more than $300,000 (ITIC, 2024). A five-minute health-check audit is a strange place to be quietly generating that number, but it happens constantly, because the endpoint feels too trivial to review carefully.
Conclusion
If NestJS's health check keeps failing on Render, resist the urge to dig through your business logic — the bug is almost always in the narrow contract between the probe and the route, not in the app itself. Confirm the path matches, keep the response fast and dependency-light, and make sure nothing global is quietly gating it with a 401. Once those three line up, the "unhealthy" status stops being a mystery and starts being, correctly, boring.
If you've already fixed this and you're rolling the same NestJS app out with zero-downtime deploys, our blue-green and rolling deployment guide covers how health checks fit into that handoff specifically — and if the service is about to run on more than one instance, horizontal scaling introduces its own health-check wrinkles worth knowing before you get there. And if you've just come from fighting a completely unrelated boot crash, the missing PORT issue on Railway is the same genre of bug wearing a different platform's name tag.
Green checkmark, quiet dashboard, on to the next problem — which, statistically, is probably also a health check somewhere else.
Frequently Asked Questions
Almost always because the health check itself is misconfigured, not because the app is actually broken. Common causes: Render is probing a path your app doesn't expose, the endpoint takes longer to respond than Render's timeout allows, or the route is unintentionally sitting behind a global authentication guard and returning 401 instead of 200. Render only knows what the probe tells it — if the probe never gets a clean response, it restarts a service that was working the whole time.
Render doesn't assume a path — you configure it explicitly in your service settings (typically something like /health or /api/health). If you never set one, or you set one that doesn't match a real route in your NestJS app, Render has nothing valid to probe and will report the service as unhealthy regardless of how well it's actually running.
Not strictly, but it's the standard, low-effort way to do it correctly. A bare route returning { status: 'ok' } technically satisfies Render, but Terminus gives you a structured way to check real dependencies (database connectivity, disk space, memory) without turning the endpoint itself into a slow, cascading failure point.
Yes, and it's one of the most common causes. If you apply a global AuthGuard at the application level, it applies to every route by default, including your health check — so Render's probe gets a 401 instead of a 200, and the platform correctly (from its perspective) marks the service as failing. The fix is excluding the health route from the guard explicitly, not disabling auth globally.
A liveness check answers one question — is the process running and able to respond at all — and should be nearly instant with no external dependencies. A full health check (database connectivity, downstream services, disk space) answers a bigger question and can legitimately take longer or fail for reasons unrelated to whether the app itself is alive. Render's automated restarts should be wired to the liveness check; a slower, deeper health check belongs on a separate route for human monitoring.
