Separating Node.js Probes: Liveness, Readiness, and Startup
Node.js services benefit from distinct liveness, readiness, and startup probes to manage dependencies and traffic effectively.
- Topic
- Node.js
- Reading time
- 6 min
- Length
- 1,264 words
- Published
- Oct 5, 2026
08:00 am IST
In this article
Keeping your Node.js service in a containerized environment running smoothly isn't just about knowing whether it's up or down. You've got to get the hang of using separate liveness, readiness, and startup probes to juggle dependencies and incoming traffic more efficiently. Taking this approach doesn't just sharpen your insights into your application's status but also cuts back on pointless restarts and keeps the service steady.
Understanding the Role of Each Probe
It’s really important to draw lines between liveness, readiness, and startup probes when you're dealing with containers. These probes each serve different purposes, and lumping them into one big check is a no-go.
Liveness Probe
The liveness probe is your go-to for verifying if the Node.js process is kicking and can still do its job. You want this check to be local and not too demanding. It should make sure your event loop is alive and kicking, not stuck in a hopeless glitch. And it definitely shouldn't tank due to stuff outside your control like databases or external APIs. You could set this up as a simple HTTP endpoint that pings back a 200 status code when things are good, or a 500 if the event loop hits a snag.
Readiness Probe
The readiness probe answers whether an instance can take on new traffic. It's about making sure dependencies needed for the next request are all green, with a strict timeout to prevent sensitive info from slipping out. Say your cache goes down; only the instances tied to it should be benched to avoid a domino effect of restarts. You might set a readiness probe to check if your database is reachable or if a critical API is responsive to ensure your service is ready to roll.
Startup Probe
Think of the startup probe as your safety net during boot-up. It gives your service the breathing room it needs to come online before liveness checks start poking. This is gold for services that take their sweet time to initialize, preventing them from being axed too soon. If your app needs extra time to load settings or make connections, this probe keeps it safe from premature liveness failures.
Practical Steps for Implementation
To get these health checks up and running, you'll need to set up your Docker or Kubernetes environment right. Here's a step-by-step:
- Configure Probes Separately: In Kubernetes, lay down
startupProbe,readinessProbe, andlivenessProbeon their own HTTP paths. Nail downfailureThresholdandperiodSecondsby closely watching startup and recovery patterns. This fine-tuning helps each probe fulfill its role without causing unnecessary restarts. - Set Reasonable Timeouts: Give each readiness probe a tight timeout to dodge chain reactions of failures. If a database usually takes 100ms to answer, a 200ms timeout can cover network delays without dragging things out.
- Log State Transitions: Keep your logs lean and mean by only noting down state changes. Capture the key stuff like probe type, state shifts, time stamps, and any errors found along the way.
- Use a Separate Monitoring Component: Have a trusty component on the side to watch readiness URLs and keep track of state changes. This example Go program shows how it can work separately from application pods, to cut down on repeated alerts and ensure your monitoring stays on point.
package main
import (
"bytes"
"context"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
)
func main() {
target := os.Getenv("READINESS_URL")
baseURL := os.Getenv("INFRAI_BASE_URL")
apiKey := os.Getenv("INFRAI_API_KEY")
metricJSON := os.Getenv("METRIC_REPORT_JSON")
if target == "" || baseURL == "" || apiKey == "" || metricJSON == "" {
log.Fatal("Environment variables are required")
}
client := &http.Client{Timeout: 2 * time.Second}
wasReady := true
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
ready := err == nil
if ready {
resp, requestErr := client.Do(req)
ready = requestErr == nil && resp.StatusCode >= 200 && resp.StatusCode < 300
if resp != nil {
resp.Body.Close()
}
}
cancel()
if ready != wasReady {
log.Printf("service=checkout check=readiness ready=%t", ready)
wasReady = ready
}
if !ready {
if err := reportMetric(client, baseURL, apiKey, []byte(metricJSON)); err != nil {
log.Printf("metric_report_error=%q", err)
}
}
}
}
Managing Trade-offs and Limitations
Splitting up these probes can make your service run smoother, but there are some caveats:
- Configuration Complexity: Setting and tuning these probes means you need to know your service dependencies and traffic flows inside out. Understanding typical startup times and which dependencies are key for readiness checks matters in setting them right.
- Logging Overhead: Even though we've cut down on logs, state transition logging still uses some resources. Aim for a balance between keeping an eye on things and keeping performance sharp. Going with centralized logging solutions could help you sift through logs effectively without dragging down performance.
- Dependency Management: You don't need every dependency in your readiness checks. Prioritize those crucial for operations. If an optional third-party service blips out, it might not justify marking the service as unready.
- Sampling and Observability: Failure numbers are few but significant. Make sure your observability tools are set up to hold onto this data just right. Prometheus or Grafana can do the job for visualizing and alerting on these metrics.
No solution fits all sizes. The setup for ten replicas might not work when you’ve dialed it down to three. Keep revisiting and tweaking these configurations, especially when there are big shifts in traffic or architecture.
Reconstructing Failures from Pages
A well-built page flags which service and environment are in trouble, reports readiness missteps over an SLO-derived threshold, and links to logs and deployments. It helps quickly pinpoint and fix issues before they hit customers. A less helpful page that just logs a non-200 response needs responders to piece together the problem while customers are stuck waiting, slowing down the fix.
Picture your checkout service with ten replicas, a new release lets four of them live but they can't reach their cache. Here, the readiness endpoint should fail those four, pulling them from service, while the liveness probe stays okay, preserving them for checking. Step one in solving this? See if the remaining capacity can bear the load and whether rolling back beats waiting out the cache issue. Restarting these processes just stirs up more trouble without solving the root cause.
This example gets at something many tutorials miss: a readiness threshold is also a capacity threshold. If losing four out of ten replicas means the rest can't hit latency SLOs, triggers should go off before the customer error rate balloons. Your specific traffic and error-budget data should define the threshold; copying a percentage from elsewhere is just guessing.
Event Logging and Metrics
Good monitoring means logging an event for each state change, not for each probe. This means capturing things like endpoint name, state changes, reasons, pod/container identity, release IDs, and timestamps. If you're already using trace_id and span_id fields, include them to link request logs. But remember, these don't automatically create a distributed trace or span tree.
Structured logging and metrics gathering are key. Log a structured entry when a check flips state, bump a failure counter with each failed evaluation, and maintain a current-state gauge for dashboards. Keep metric labels within reason; useful ones include service, environment, check, and reason, but leave out request IDs, pod UIDs, and raw error text.
The Go program example shows a standalone monitoring component that checks the readiness URL, logs state shifts, and reports a given metric document through an observability API on failures. Run this component as a single, highly available entity, not alongside each pod, to avoid doubling up on incident reports.
Sources
5 Docker and Kubernetes Readiness, Liveness, Startup Probe Lessons — Node.js
Every claim above was checked against this source before publishing. The analysis, the code and the opinions are mine.
Frequently asked
Why should liveness, readiness, and startup probes be separate?
Each probe serves a different purpose: liveness ensures the process is healthy, readiness checks if the instance can handle traffic, and startup allows initialization without premature liveness checks.
What are the risks of combining these probes?
Combining probes can lead to unnecessary restarts and traffic disruptions if a non-critical dependency fails, as all checks may fail at once.
How often should probes be configured to run?
This depends on your application's behavior and recovery times, but generally, they should run frequently enough to catch issues without causing excessive load or false alarms.
Can these probes be used outside of Kubernetes?
Yes, Docker's HEALTHCHECK can be configured similarly, though it doesn't manage traffic routing like Kubernetes does.