Why Your Service Should Have Two Health Checks Not One
A service needs two health checks because liveness and readiness answer different questions. Liveness asks should this process be killed and restarted. Readiness asks should this process receive new traffic. A single endpoint that mixes the two will cause unnecessary restarts during transient dependency failures and is one of the most common quiet sources of self-inflicted outages.
Written by Yashveer Singh, founder of Yashveer Labs.
What you actually need to know
- One health check is doing two jobs and the jobs contradict each other.
- Liveness answers "should I kill this process," readiness answers "should I send it traffic."
- A combined check that depends on the database will trigger cascading restarts during a transient database issue.
- Readiness can call dependencies. Liveness cannot.
- Add a startup probe for any service that takes more than a few seconds to warm up.
| Check type | Purpose | What it should check | What action it triggers |
|---|---|---|---|
| Liveness | Process alive? | Process responding, event loop alive | Kill and restart |
| Readiness | Ready for traffic? | Process initialized, dependencies reachable | Stop or start routing traffic |
| Startup | Finished booting? | Process completed its startup sequence | Delays liveness and readiness checks |
The core argument
The single /health endpoint pattern is a habit that comes from an era before container orchestration. When a service ran on a fixed VM behind a load balancer, you only had to answer one question: is this thing okay. The platform did not restart processes. It just stopped sending traffic.
Now we have orchestrators that kill pods aggressively, and we have load balancers that route traffic conditionally, and these two systems need different answers. The orchestrator wants to know if it should give up on this process. The load balancer wants to know if this process can take traffic right now. They are different questions with different consequences if you get them wrong.
I have walked into several incidents where the root cause was a single health check that returned unhealthy because a database connection was briefly slow. The orchestrator killed the pod. The new pod started, hit the same slow database, returned unhealthy, got killed. The team had built itself a restart storm that took the whole service down during a five-second database blip that the database had already recovered from.
The fix is structural. Two endpoints. Different rules. Different consumers.
What each check should actually do
Liveness
Liveness is the simplest possible check that confirms the process is running and capable of responding. It does not touch the database. It does not call other services. It does not do anything that could fail because of something outside this process.
A liveness check that returns 200 means "I am here and answering." A liveness check that fails means "I am stuck or crashed, please restart me." It is the cheapest possible signal because the action it triggers, restarting the process, is destructive and you do not want false positives.
Readiness
Readiness is more expensive. It can check the database connection, the cache, the message queue, anything the service genuinely needs to handle a request. If readiness returns unhealthy, the load balancer stops sending new traffic. Existing connections drain. The pod is not killed. It just sits there waiting until it can serve again.
This is the right behavior when a dependency is slow. Stop sending traffic to instances that cannot handle it. Do not kill those instances, because killing them does not fix the dependency.
Startup
Startup is the trickiest because it only runs at boot. Use it for services that take significant time to initialize, like ones that load a large model or warm a sizable cache. Without it, you risk the orchestrator killing a slow-starting pod for failing liveness before it ever finished starting.
How long does it take to add
| Service type | Effort | Risk if you skip |
|---|---|---|
| New service | An hour | Adopt the right pattern from day one |
| Existing simple service | Half a day | Restart storms during dependency issues |
| Existing complex service | A day | Cascading failures across pods |
| Legacy service in maintenance | A day plus testing | Hard to add later but worth it |
What each check must include
- Liveness: only checks internal to the process. No network calls. Fast and cheap.
- Readiness: checks dependencies the service actually needs to serve requests.
- Startup: tracks initialization progress.
- All three: return explicit status codes and minimal bodies. Detail goes in logs, not response bodies.
- Consistent paths: /healthz/live, /healthz/ready, /healthz/startup is a common convention.
Expert opinion
The first time I separated liveness from readiness in production, the team was skeptical. It felt like over-engineering for a problem we had not had. Three months later a Redis instance had a slow minute and our combined check would have restarted every pod in the fleet. Instead, readiness flipped to false for the duration, the load balancer stopped sending traffic, the pods stayed alive, and when Redis came back the fleet resumed without a single restart. That is the day the pattern earned its keep.
>
Yashveer Singh, founder of Yashveer Labs
How this played out on a real project
A SaaS platform I worked on had a single /health endpoint that called the database. During business hours the database had occasional 200ms spikes from a long-running report query. Each spike caused the health check to time out. The orchestrator restarted pods. The new pods hit the same database, timed out, got restarted. The error rate during these spikes was much worse than the database issue would have caused on its own.
We split the check into liveness and readiness. Liveness became a trivial in-process check. Readiness kept the database call but with a generous timeout and a small failure tolerance. The next time the database spiked, readiness briefly returned false on a few instances, the load balancer rerouted, and not a single pod was restarted. The platform's self-inflicted outage pattern disappeared. The discipline of separating concerns showed up again in the resilience patterns of circuit breakers and retries, which addresses related failure modes.
Common mistakes
- Using a single /health endpoint for both consumers.
- Calling the database from the liveness check.
- Returning 500 instead of 503 for unhealthy, which mixes application errors with health signals.
- Setting liveness probes too aggressively, causing healthy slow-starting pods to be killed.
- Setting readiness probes too leniently, so traffic continues to a broken instance.
- Not adding a startup probe for slow-starting services.
- Putting expensive logic in a readiness check that fires every second. The check becomes a load source of its own.
A two week plan to fix it
- Day one. Audit your current health check. What does it actually check? Who consumes it?
- Days two and three. Split into three endpoints. Liveness, readiness, startup.
- Days four and five. Update the orchestrator and load balancer configs to use the new endpoints with sane intervals and thresholds.
- Week two. Roll out across one service first. Watch the behavior during the next minor dependency hiccup.
- Week two ongoing. Roll out to the rest of the fleet. Document the pattern in your architecture decision records so the next service inherits it.
- Long term. Make the three-endpoint pattern part of the new service template so this is never a question again.
Frequently asked
Why I am built for this project type
I have worked on five production systems before turning eighteen. That is not a flex. That is a statement of capability. Yashveer Singh, founder of Yashveer Labs. The work in this article is the work I do on a weekly basis. If you are facing the problem I just described, I do not need to be sold on solving it. I need to be told the constraints.
Posts that line up with this one.
- Backend, APIs, and System Design
Idempotency Keys: A Pattern Every Senior Engineer Should Master
Idempotency keys are a small implementation with an outsized impact on system reliability. Here is the pattern, the edge cases, and the production pitfalls that most introductions skip.
- Backend, APIs, and System Design
JSON Columns in Postgres: When They Make Sense
JSON columns in Postgres are genuinely useful for flexible, semi-structured data. They are also frequently misused as a shortcut to avoid schema design. Here is when to use them and when to use normalized tables instead.
- Backend, APIs, and System Design
Kafka in 2026: When You Need It and When You Do Not
Kafka is powerful, but most startups reach for it before they need it. Here is how to decide.
- Backend, APIs, and System Design
Lambda Cold Starts: Why They Still Matter in 2026
Cold starts have improved significantly but have not been eliminated. Here is the current state of cold start latency, which use cases still require mitigation, and the practical patterns that keep them from affecting users.