Express is still the workhorse of the Node.js world — most APIs, backends-for-frontends, and internal services that power production systems are a few hundred lines of Express routes in a trench coat. But a Node deployment is more than one process listening on a port: it is pm2 or a container orchestrator supervising workers, a database pool with a small default size, usually Redis, a reverse proxy or load balancer in front, and background jobs running in node-cron, BullMQ, or Agenda. Any of those can fail while GET / keeps returning 200 — and Node's single-threaded event loop adds a failure mode where one blocking call quietly degrades every request at once.
This guide walks through monitoring an Express app the way it actually breaks: a proper health endpoint (Express doesn't ship one), the Node-specific failure modes worth alerting on — including the infamous keep-alive 502 — background job coverage, and a concrete CronAlert setup you can copy. CronAlert is agentless and runs on Cloudflare's edge, so everything here is just HTTP checks, content assertions, and heartbeats — nothing to npm install.
Build a health endpoint (Express doesn't ship one)
Like FastAPI — and unlike Rails or Laravel with their built-in /up routes — Express leaves health checks to you. Add a GET /healthz route that verifies the dependencies your app cannot serve traffic without and returns 200 when they pass, 503 when any fail. For the general pattern — shallow vs deep checks, what belongs in each — see our guide to HTTP health check endpoints.
A good Node health check does three things. First, it runs a trivial query — SELECT 1 — through the same pg or Prisma pool your routes use, which confirms both that the database is reachable and that the pool can still hand out a connection. Second, it round-trips Redis with a PING. Third, it touches any external service the app is useless without. Wrap each in try/catch, give the whole handler a short internal timeout (a health check that hangs is worse than one that fails), and return a tiny JSON body free of diagnostic detail — or gate a richer version behind a static token your monitor sends in a header. Our database health endpoint deep-dive covers the query-level details.
One Node-specific tip: register the health route before any auth middleware, and keep it out of any catch-all error handler that rewrites status codes — a 503 that gets swallowed into a friendly 200 error page defeats the whole exercise.
What to monitor in an Express app
- The homepage or API root. Whatever real clients hit first, checked for a 200 and for expected content — not just that something responded.
- Your
/healthzendpoint. The dependency-aware signal that catches database and pool trouble. - Your most important endpoints. The two or three routes your product actually depends on, each with a keyword assertion on the JSON — a field name that only appears in a correct response. See monitoring API endpoints for assertion patterns.
- HTTPS and the SSL certificate. An expired certificate takes down every client no matter how healthy the process is; see SSL certificate monitoring.
- A heartbeat per background worker or scheduler. Covered below — this is the half of Node monitoring most teams skip.
All of this fits comfortably inside CronAlert's free plan — 25 monitors at a 3-minute interval with SSL and content checks included. Pro tightens the interval to 1 minute and unlocks keyword monitoring across every monitor.
Node-specific failure modes
Generic uptime advice misses the ways Node apps actually break. Design your checks around these:
- Event-loop blocking. One synchronous call in a request path —
fs.readFileSync,JSON.parseon a multi-megabyte payload, sync crypto, an accidental hot loop — stalls every request on the process. The signature from outside: all endpoints slow down together, then time out together. A response-time threshold catches it while it is still degradation. - The keepAliveTimeout 502. Node's default
server.keepAliveTimeoutis 5 seconds; most load balancers hold idle connections for 60+. When Node closes a socket the balancer expected to reuse, the next request 502s. The result is sporadic, maddening 502s that no log line explains — raisekeepAliveTimeout(andheadersTimeout) above the balancer's idle timeout. - Crash-restart loops. Since Node 15, an unhandled promise rejection kills the process. pm2 or Kubernetes restarts it, hiding the crash — but every cycle is a few seconds of 502s, forever. External checks see the pattern;
pm2 statussays everything is fine. - Memory leaks ending in OOM. A leaking cache or listener accumulation grows the heap until the kernel or orchestrator kills the process. Response times creep up as garbage collection thrashes first — another reason to alert on latency, not just status.
- Pool exhaustion.
pg's default pool is 10 connections; one forgottenclient.release()in an error path and every request eventually queues on a pool that never frees. Only a health check that actually acquires a connection sees it. - Error middleware lying. A catch-all Express error handler that renders a friendly page — with status 200 — turns every crash into "up" as far as a status-code check is concerned. Assert on content, not just codes.
- Stalled queues and schedulers. BullMQ workers die or Redis restarts and jobs pile up unprocessed; node-cron stops firing after a deploy replaced the process at the wrong moment. Nothing errors — work just stops happening.
Most of these never change your homepage's status code. That is the recurring lesson across every stack we have covered: status codes lie, and you need dependency-aware checks, content assertions, and response-time thresholds to see the truth.
Monitoring background work: node-cron, BullMQ, and Agenda
Background processing is where Node monitoring most often falls short. Your API can be flawless while every BullMQ worker is dead, the queue is backing up in Redis, or the node-cron schedule silently stopped when a deploy recycled the process — and no HTTP check will notice, because nobody makes a request that fails. Emails stop, exports stop, syncs stop, and you find out from a customer.
The fix is heartbeat monitoring: your job reaches out instead of the monitor reaching in. At the end of every successful run, the job makes a quick fetch to a unique CronAlert heartbeat URL. You configure the expected interval plus a grace period, and if the ping stops arriving, CronAlert alerts you that the pipeline went silent — silence is the alarm. A practical setup is one lightweight scheduled job on the same cadence as your most critical periodic work, whose only task is to confirm it can reach Redis and the database, then ping the heartbeat.
Our guides to cron job heartbeat monitoring and background worker monitoring cover the patterns in depth, and for one-shot jobs see batch job monitoring. Heartbeats are on every CronAlert plan, including free.
Catching "up but wrong" with content checks
An Express response can be a perfect 200 and still be wrong: an error handler swallowing exceptions into a friendly page, an endpoint returning an empty array because a dependency call failed quietly, a stale cache, or a misrouted deploy serving the wrong service entirely. To catch these, assert on the response body.
For JSON APIs the pattern is simple: require a string that only a correct response contains — a known field name, an expected enum value, a version marker. If an error payload takes its place, the keyword disappears and the check fails despite the 200. You can also assert that an error marker is absent, so a leaked stack trace or default error JSON trips the alarm. Regex matching (Pro) handles dynamic payloads, and for endpoints that should be byte-stable, a SHA-256 content-hash check alerts the instant output drifts. See the keyword monitoring guide for building robust assertions.
Response-time thresholds matter more on a single thread
In a multi-threaded runtime, one slow endpoint mostly hurts itself. On Node's single thread, event-loop blocking makes slowness global — which means response time is not just a performance metric but an availability early-warning. Set a response-time threshold on your health endpoint and key routes so a creep past, say, two seconds alerts you while the app is still up. The same threshold catches GC thrash from a memory leak, degrading databases, and saturated pools before they become timeouts. Our timeout thresholds guide covers picking numbers that alert early without generating noise.
A concrete CronAlert setup
Here is an end-to-end setup you can replicate in a few minutes after you create a free CronAlert account:
- Add a
/healthzroute.SELECT 1through your pool, RedisPING, 200/503, registered before auth middleware and outside any status-rewriting error handler. - Create an HTTP monitor for it. Point CronAlert at
https://api.yourapp.com/healthz, expect 200, set the interval (3 minutes free, 1 minute on Pro). - Monitor your key endpoints with JSON keyword assertions. Require a field name only present in a correct response.
- Add a heartbeat per queue and scheduler. A periodic BullMQ, Agenda, or node-cron job pings its CronAlert heartbeat URL on every successful run, with a sensible grace period.
- Turn on SSL and response-time checks. Certificate monitoring on the domain; a response-time threshold on the health endpoint and key routes.
- Fix your keep-alive timeouts while you're at it. Set
keepAliveTimeoutabove your load balancer's idle timeout so your new monitors don't dutifully report the 502s you could have prevented. - Wire up alert channels. Email, Slack, Discord, Teams, Telegram, PagerDuty, Opsgenie, Splunk On-Call, webhooks, or PWA push.
Every plan includes the full REST API — natural for Node developers; your whole monitor fleet is a few fetch calls — and CronAlert ships an MCP server so Claude Code, Cursor, or Windsurf can manage monitors for you. Teams needing multi-region coverage can move to the Team plan, which checks from five edge regions with quorum so a single regional blip never pages you.
Frequently asked questions
Does Express have a built-in health check endpoint?
No — you add your own. A GET /healthz route that runs SELECT 1 through your pool, pings Redis, and returns 200/503 takes minutes to write. Register it before auth middleware and keep it out of error handlers that rewrite status codes.
Why does my Node.js app return intermittent 502s behind nginx or a load balancer?
Usually the keep-alive mismatch: Node's default keepAliveTimeout (5s) is shorter than your balancer's idle timeout (60s+), so Node closes sockets the balancer still trusts. Raise keepAliveTimeout and headersTimeout above the balancer's idle timeout.
Why does my whole Express app slow down or time out at once?
Event-loop blocking: one synchronous call — fs.readFileSync, giant JSON.parse, sync crypto — stalls every request on the process. A response-time threshold catches the global degradation early; move CPU-heavy work to worker threads or a queue.
How do I monitor node-cron, BullMQ, or Agenda background jobs?
Heartbeats: each job pings a unique CronAlert heartbeat URL on every successful run, and silence beyond the grace period triggers an alert — catching dead workers, stalled queues, and schedulers that stopped firing.
Should I monitor my Node app if pm2 already restarts it on crashes?
Yes. Restart supervision hides crashes rather than eliminating them: a crash loop is a few seconds of 502s on every cycle, indefinitely. External monitoring sees what users see and pushes you to fix the underlying unhandled rejection or OOM.
Start monitoring your Express app
Node apps fail in ways a single ping never catches: a blocked event loop degrading everything at once, keep-alive 502s that appear in no log, crash-restart loops hidden by pm2, dead queue workers, and 200s wrapping error pages. A deep health endpoint, keyword assertions on key routes, heartbeats on background work, and SSL plus response-time checks give you a signal you can trust — and all of it fits in CronAlert's free plan. Create a free CronAlert account and point your first check at /healthz in the next five minutes.
Related reading: HTTP health check endpoints, building a database health endpoint, Next.js and Vercel monitoring, and our companion guides for Django, Rails, Laravel, and FastAPI applications.