Health check endpoint in Next.js
Create app/api/health/route.ts with a GET handler that runs one cheap query against your database, returns 200 when it succeeds and 503 when it throws, and add export const dynamic = "force-dynamic" so the response is never served from a cache.
A health endpoint exists so something outside your app can ask one question and get an honest answer: can this instance serve a request right now. The Next.js server is a poor judge of that on its own. It stays up and keeps rendering long after the database has stopped accepting connections, so a route that returns ok: true unconditionally will report green for the entire length of an outage while your users stare at 500s.
That is why the handler has to touch the thing it cannot work without. One query that reads nothing, select 1, and a 503 when it throws. Logdash flips a monitor to down on any status code outside 200 to 399 and fires the alert on that transition, so the 503 is not decoration - it is the entire mechanism by which the route becomes a page you get woken up by.
The route handler
import { db } from '@/lib/db';
// Never prerender or cache a health check.
export const dynamic = 'force-dynamic';
export async function GET() {
try {
await db.execute('select 1');
return Response.json({ ok: true });
} catch {
// 503: the process is alive, the app is not servable.
return Response.json({ ok: false, error: 'db' }, { status: 503 });
}
} Two details in that handler are deliberate. The query is the cheapest one the driver can send, so a check costs a connection and a round trip and nothing more. And the failure body has two fields, because the only consumer that matters never opens it.
What to check and what to leave out
- The database, with a query that reads no rows. A count over a real table is a load test you would then be running every 5 minutes forever.
- Anything a request genuinely cannot complete without: Redis if sessions live there, the queue if the handler enqueues work.
- Nothing you do not own. A health route that calls Stripe hands Stripe the ability to wake you at 3am about an outage you cannot fix.
- Under a second, end to end. The Logdash pinger gives up after 10 seconds and records the check as down, and a route with a slow query is slowest exactly when the system is already in trouble.
- No commit hash, no environment dump, no dependency versions. The URL is public and the monitor only ever reads the status code.
Point a monitor at it
- 1 Add the URL Create a project in Logdash and give the monitor https://yourapp.com/api/health. The first check runs straight away, so a wrong path shows up in the next few seconds rather than during an incident.
- 2 Choose the interval Every 5 minutes on the free plan, every minute on Builder, every 15 seconds on Pro. Each check records the status code and the response time, so the latency chart builds itself with no extra work.
- 3 Break it before production does Stop the database and leave the app running. The route starts returning 503, the monitor flips to down on the next check, and a Telegram alert arrives naming the endpoint and the code it got back.
The Next.js trap: a cached health check
Since Next.js 15, GET route handlers are dynamic by default, and force-dynamic is belt and braces. On 13 and 14 it was the opposite: a GET handler with no request-time API in it got prerendered at build, so the health route was frozen to whatever it returned on the build machine and answered 200 forever, including through outages that lasted days. Keep the export. It is one line and it survives an upgrade in either direction. Watch the CDN as well, since a health URL that Vercel or Cloudflare can answer from cache is a URL your monitor has stopped measuring. On the Pages Router none of this applies, because API routes always run per request.
import type { NextApiRequest, NextApiResponse } from 'next';
import { db } from '@/lib/db';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
try {
await db.execute('select 1');
res.status(200).json({ ok: true });
} catch {
res.status(503).json({ ok: false, error: 'db' });
}
} What is the default health check path in Next.js?
There is not one. Next.js ships no health route, so you write it yourself and the convention people land on is /api/health, which is app/api/health/route.ts on the App Router and pages/api/health.ts on the Pages Router.
What status code should a Next.js health check return?
200 when the app can serve traffic and 503 when it cannot. Logdash treats anything outside 200 to 399 as down, so a 503 is what turns a broken dependency into an alert. Do not return 200 with an error field in the body - nothing reads the body.
What is the difference between a Kubernetes liveness and readiness probe for Next.js?
Liveness asks whether the process should be restarted, so it checks nothing external - a plain 200 is right. Readiness asks whether this pod should receive traffic, so that is the one that runs select 1 and returns 503. Point liveness at a bare route and readiness at /api/health, or a database blip will restart every pod at once.
What health check path should I set for Next.js on ECS?
Set the ALB target group health check path to /api/health and widen the success matcher if you have narrowed it, because a 503 has to read as a failure and a 200 as a pass. Keep the container health check separate and cheap so ECS is not restarting tasks over a database that is briefly slow.
Should the health endpoint be authenticated?
Leave it open. An auth check in front of it means the monitor needs a credential, and a rotated token then reads as an outage. Keep the body to one boolean instead, and put anything detailed behind a second, authenticated route.