liveness and readiness endpoints for Kubernetes
Kubernetes decides whether a pod needs restarting or should be pulled out of load balancing based on health checks, but those two decisions need different information. A liveness check only answers whether the PHP process is running at all, while a readiness check has to verify the application can actually handle traffic meaningfully, for example whether the database connection is up. Answering both checks through the same endpoint risks either unnecessary restarts or sending traffic to pods that aren't actually ready.
Table of Contents
- 1. The difference between liveness and readiness
- 2. A dedicated, unauthenticated health endpoint
- 3. Readiness check for the database connection
- 4. Checks for Redis and external dependencies
- 5. A central HealthCheckRegistry for extensible checks
- 6. Configuring matching Kubernetes probes
- 7. Graceful shutdown: readiness during pod termination
- 8. Startup probes for slow-starting applications
- 9. Common mistakes in practice
- 10. Summary
- 11. FAQ
1. The difference between liveness and readiness
A liveness probe answers a very simple question: is the process still running and does it respond to requests at all? If the liveness probe fails repeatedly, Kubernetes assumes the container is stuck in an unrecoverable state, say a deadlock or an infinite loop, and restarts the container. A liveness probe should therefore be kept deliberately minimal and should not check external dependencies, since a database outage is no reason to restart an otherwise healthy PHP process.
A readiness probe, on the other hand, answers whether the pod is currently able to handle incoming traffic meaningfully. If it fails, Kubernetes removes the pod from the service endpoint without restarting the container, and stops routing traffic there until the probe succeeds again. That's useful, for instance, during startup while the application is still establishing connections, or during a temporary outage of a dependency like the database, without the pod itself being considered broken.
2. A dedicated, unauthenticated health endpoint
Health endpoints should sit outside the regular firewall and authentication configuration, since Kubernetes probes request them without session cookies or API tokens. In security.yaml, this typically means a dedicated firewall entry with a pattern matching /health(/.*)? and security: false, so these routes bypass any authentication check entirely while still being served through Symfony's normal routing and controller system.
It matters to keep the health endpoint deliberately lean and avoid exposing sensitive information. A readiness response should return a simple status like {"status": "ok"} on success, and on failure should communicate which dependency failed without leaking internal details like connection strings or stack traces, since the endpoint is reachable publicly without authentication.
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Health\HealthCheckRegistry;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
/**
* Liveness and readiness endpoints for Kubernetes probes.
*/
final class HealthController
{
public function __construct(private readonly HealthCheckRegistry $registry)
{
}
#[Route('/health/live', name: 'health_live', methods: ['GET'])]
public function live(): JsonResponse
{
// Liveness: just check whether the process responds at all.
return new JsonResponse(['status' => 'ok']);
}
#[Route('/health/ready', name: 'health_ready', methods: ['GET'])]
public function ready(): JsonResponse
{
$results = $this->registry->runAll();
$healthy = array_reduce(
$results,
static fn (bool $carry, bool $ok): bool => $carry && $ok,
true,
);
return new JsonResponse(
['status' => $healthy ? 'ok' : 'unavailable', 'checks' => $results],
$healthy ? 200 : 503,
);
}
}
3. Readiness check for the database connection
A database check shouldn't just pull the Doctrine connection out of the container, it should actively verify a connection can actually be established. A minimal query like SELECT 1 is common, since it needs no table access at all and therefore still works even if the schema hasn't been fully migrated yet. A short timeout for this check matters, so a hanging database connection doesn't block the entire readiness probe and force Kubernetes to wait longer than necessary for a response.
For the connection attempt itself, it's worth catching any resulting exceptions deliberately and translating them into a simple boolean result instead of letting them propagate uncaught to the controller. That way, the readiness response stays a clean JSON payload with HTTP status 503 even during a database outage, rather than showing an unformatted error page with potentially sensitive stack trace information.
4. Checks for Redis and external dependencies
For Redis, say as a cache or session backend, a simple PING command works well, returning PONG on success and thereby confirming both network reachability and basic operational health. Here too, a short timeout prevents a hanging Redis server from unnecessarily delaying the entire readiness probe.
For external dependencies like third-party APIs, it's worth considering whether an outage of that dependency should really mean the pod is marked not ready. If the external API is only needed for a secondary feature, say an optional shipment tracking lookup, its failure shouldn't fail the whole readiness probe, since that would pull the entire pod out of load balancing even though core functionality keeps working fine.
5. A central HealthCheckRegistry for extensible checks
Instead of wiring individual checks directly into the controller, a HealthCheckRegistry is a good fit: it collects individual check classes through a shared HealthCheckInterface, typically via Symfony service tagging. Each check implements a single method that returns true or false, and gets its dependencies, say the Doctrine connection or the Redis client, through normal constructor injection.
This approach makes it easy to add new checks without touching the controller itself: a new check class gets written, registered as a service with the matching tag, and the registry automatically picks it up on the next request. That's particularly useful in growing system landscapes with multiple external dependencies, since the health endpoint evolves organically alongside the application's actual dependencies.
6. Configuring matching Kubernetes probes
In the pod spec, livenessProbe and readinessProbe are configured as separate httpGet blocks, each with its own path, so /health/live for the liveness probe and /health/ready for the readiness probe. initialDelaySeconds and periodSeconds matter too: too short an initial delay can make Kubernetes restart a pod that's simply still starting up, while too long a periodSeconds means an actual outage gets detected late.
A common mistake is setting failureThreshold too low for the liveness probe, causing brief load spikes that temporarily increase response time to get misclassified as an outage and trigger an unnecessary restart. A higher threshold on the liveness probe compared to the readiness probe usually makes sense, since restarting is a far more drastic action than temporarily pulling a pod out of load balancing.
7. Graceful shutdown: readiness during pod termination
An often overlooked aspect of readiness probes is behavior during the pod's actual termination. When Kubernetes terminates a pod, it first sends a SIGTERM signal and waits up to terminationGracePeriodSeconds before hard-killing the process with SIGKILL. Things get problematic when Kubernetes still treats the pod as ready in parallel and keeps routing traffic to it while the application has already started shutting down, which can result in aborted requests as in-flight connections get cut off mid-processing.
The usual fix is a preStop hook that inserts a short delay before the actual SIGTERM, typically a few seconds, giving the change to the service endpoint time to propagate across the cluster before the process actually terminates. In addition, the readiness probe can be implemented to immediately return false as soon as a SIGTERM arrives, even while the process itself is still running, so Kubernetes pulls the pod out of load balancing as quickly as possible while already in-flight requests can still finish processing in an orderly way.
8. Startup probes for slow-starting applications
For Symfony applications with a heavy boot process, say due to a large container compilation step or cache warmup, an additional startupProbe can help. It suppresses liveness and readiness probes until the application has responded successfully once, preventing Kubernetes from misinterpreting a longer but perfectly normal startup as a hang.
The startupProbe can reuse the same endpoint as the liveness probe, but should be configured with more generous failureThreshold and periodSeconds values, since it's deliberately meant to allow more time for the initial start, without the regular liveness probe needing to show the same leniency once the application is actually running.
9. Common mistakes in practice
A widespread mistake is using the same endpoint for both liveness and readiness and checking the database connection inside it. If the database briefly goes down, Kubernetes then treats that as a liveness failure and restarts every pod, even though a restart doesn't fix the actual problem, the database outage, and instead adds extra instability from simultaneous restarts.
Another common mistake is accidentally leaving the health endpoint behind the regular authentication layer. Kubernetes probes don't send credentials, so such an endpoint always responds with HTTP 401, and Kubernetes then permanently marks the pod as not ready or not alive, even though the application is fully functional internally.
| Probe type | Checks | On failure | Typical checks |
|---|---|---|---|
| Liveness | process responds | container restart | simple HTTP 200 response |
| Readiness | traffic readiness | removal from service | DB, Redis, critical dependencies |
| Startup | boot completed | suppresses other probes | like liveness, with more patience |
| Health endpoint | both states | depends on route | separate paths /health/live, /health/ready |
Mironsoft
Symfony architecture, clean domain logic, and legacy modernization
Symfony applications that stay maintainable two years down the line?
We review existing Symfony projects for bloated controllers, missing service abstractions, and untested core logic, then build an architecture that absorbs new features without getting more fragile with every release.
Architecture Review
Checking bundle structure, dependency injection, and service abstractions for maintainability.
Legacy Modernization
Incrementally migrating outdated Symfony versions without a full rewrite.
Testing and Quality Assurance
Setting up PHPUnit, PHPStan, and CI pipelines for lasting code quality.
10. Summary
Health Checks for Kubernetes: Key Facts
Liveness
minimal, only checks whether the process responds at all
Readiness
checks real traffic readiness including database and Redis
Endpoint
dedicated, outside the firewall authentication
Registry
extensible HealthCheckRegistry instead of hard-wired checks