Health Checks with Liveness and Readiness Probes
A misconfigured health check is more dangerous than none at all. Kubernetes restarts healthy pods because a liveness probe was written too strictly, or sends traffic to a pod whose database connection dropped long ago. This article shows how a health check for Symfony in Kubernetes becomes truly reliable, with liveness, readiness and startup probes working together.
Table of Contents
- 1. Why a health check in Kubernetes decides operational stability
- 2. Liveness probes: when Kubernetes restarts a container
- 3. Readiness probes: when a pod actually receives traffic
- 4. Startup probes for slow starting Symfony applications
- 5. Building a dedicated health check endpoint in Symfony
- 6. Checking dependencies: database, cache and message queue
- 7. Configuring probes correctly in the Kubernetes manifest
- 8. Common pitfalls: timeouts, cascades and false alarms
- 9. Liveness, readiness and startup probe compared
- 10. Summary
- 11. FAQ
1. Why a health check in Kubernetes decides operational stability
Kubernetes makes automated decisions about a pod's life and traffic based on a health check: restart it, remove it from the load balancer, or keep waiting. These decisions happen without human intervention, which is exactly why the quality of the underlying health check matters so much. A check that is too simple, only confirming PHP FPM responds, says nothing about whether the application can actually answer database queries.
Conversely, a health check that is too strict is just as dangerous. If the liveness probe also checks the database connection, a brief network hiccup on the database side causes Kubernetes to restart entire rows of healthy application containers, even though the actual problem lies elsewhere. A well designed health check for Symfony therefore strictly separates the question of whether the process itself is still alive from the question of whether it is currently able to answer requests meaningfully.
This article covers exactly that separation: liveness probe, readiness probe and startup probe have different jobs, different consequences on failure, and should therefore use different endpoints or at least different check depths. A single, undifferentiated health check for all three probe types is one of the most common mistakes in Symfony Kubernetes setups.
2. Liveness probes: when Kubernetes restarts a container
The liveness probe answers a single question: is the process inside the container still in a state it can recover from on its own, or is it stuck in a deadlock or infinite loop from which only a restart helps. If the liveness probe fails repeatedly, Kubernetes kills the container and restarts it. That is exactly why this health check should be as minimal as possible: it should only confirm the PHP process itself is responding, never whether external dependencies are reachable.
A typical Symfony mistake is a liveness probe that reuses the same route as the readiness probe and thereby also checks the database connection. If the database goes down for ten seconds, Kubernetes suddenly kills all application pods at once, even though not a single process was actually stuck. The database comes back, but all pods restart at the same moment, artificially extending the downtime instead of shortening it. The liveness probe in a clean health check setup therefore checks only the local process state.
<?php
// src/Controller/HealthController.php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
final class HealthController
{
// Liveness endpoint: only confirms the PHP process itself responds.
// No external dependency check here — a slow database must never
// cause Kubernetes to kill and restart otherwise healthy pods.
#[Route('/health/live', name: 'health_live', methods: ['GET'])]
public function live(): JsonResponse
{
return new JsonResponse(['status' => 'ok'], 200);
}
}
3. Readiness probes: when a pod actually receives traffic
The readiness probe answers a different question: is the pod currently able to handle incoming traffic meaningfully. If this health check fails, Kubernetes removes the pod from the service endpoint without killing it. The container keeps running but receives no new traffic until the readiness probe succeeds again. That is the decisive difference from the liveness probe, and the reason external dependencies may and should be checked here.
For Symfony this means: the readiness probe checks whether the database connection is up, whether the cache layer is reachable, and, where relevant, whether a message queue connection can be established. If one of these dependencies fails, the pod removes itself from rotation while other, healthy pods keep receiving traffic. This prevents user requests from landing on a pod that could only respond with an error anyway, without a single container needing to be restarted.
4. Startup probes for slow starting Symfony applications
Symfony applications with a large container, many bundles or a cold OPcache can take several seconds to minutes before the first request is reliably answered. Without a startup probe, Kubernetes wrongly interprets a slow starting application as a failed liveness probe and kills the container before it has even finished booting. This leads to a restart loop the pod never escapes, because every restart requires the same startup time again.
The startup probe solves this problem by disabling liveness and readiness probes until the first successful startup check arrives. Only after that do the regular probes take over monitoring. For a health check setup with Symfony, the startup probe is therefore not an optional detail but, in environments with noticeable boot time, a necessary component to avoid restart loops on every deployment and every node change.
# k8s/deployment.yaml — probes section for a Symfony deployment
containers:
- name: symfony-app
image: registry.mironsoft.de/symfony-app:1.4.2
ports:
- containerPort: 9000
# Startup probe: gives the container time to boot before
# liveness/readiness even start evaluating
startupProbe:
httpGet:
path: /health/live
port: 8080
failureThreshold: 30
periodSeconds: 2
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
5. Building a dedicated health check endpoint in Symfony
A clean health check endpoint in Symfony does not belong in the application namespace of the business logic, but in its own lightweight controller that deliberately touches no other services of the application besides the ones explicitly meant to be checked. It is also important that this route does not run through middleware for authentication, session handling or CSRF protection, since those layers themselves introduce dependencies such as session storage or Redis, unnecessarily complicating the health check.
In practice, a dedicated firewall section in the Symfony security configuration that completely exempts health routes from authentication is recommended. This keeps the endpoint reachable even when the session infrastructure itself is having problems, which is essential for a reliable health check. In addition, the endpoint should allocate as little memory as possible and avoid instantiating heavy services from the container that are not needed for the actual check.
# config/packages/security.yaml — exempt health routes from authentication
security:
firewalls:
health:
pattern: ^/health
security: false
stateless: true
main:
lazy: true
provider: app_user_provider
6. Checking dependencies: database, cache and message queue
The readiness endpoint typically checks three categories of dependencies: the primary database connection through a minimal query such as SELECT 1, the cache layer through a simple ping to Redis or Memcached, and optionally the reachability of a message queue such as RabbitMQ. Each of these checks should have a tight timeout under one second, so a hanging check does not itself become the problem and block the entire probe.
It is also crucial that a single failed dependency check does not necessarily have to mark the entire health check as red. If a message queue for an asynchronous reporting job is briefly unreachable while the database and cache work fine, it may be more sensible to keep the pod available for synchronous HTTP requests and only trigger an internal alert instead of removing it from rotation entirely. This trade off depends on the specific application and should be made deliberately, not implicitly through a blanket all or nothing check.
<?php
// src/Service/HealthCheckService.php
declare(strict_types=1);
namespace App\Service;
use Doctrine\DBAL\Connection;
use Symfony\Component\Cache\Adapter\RedisAdapter;
final class HealthCheckService
{
public function __construct(
private readonly Connection $connection,
private readonly \Redis $redis,
) {
}
/**
* Runs all readiness checks with a strict timeout per dependency.
*
* @return array<string, bool>
*/
public function checkReadiness(): array
{
return [
'database' => $this->checkDatabase(),
'cache' => $this->checkCache(),
];
}
private function checkDatabase(): bool
{
try {
$this->connection->executeQuery('SELECT 1');
return true;
} catch (\Throwable) {
return false;
}
}
private function checkCache(): bool
{
try {
return $this->redis->ping() !== false;
} catch (\Throwable) {
return false;
}
}
}
7. Configuring probes correctly in the Kubernetes manifest
Beyond the mere existence of the probes, fine tuning decides the stability of a health check setup. periodSeconds determines how often the check runs, timeoutSeconds how long it waits for a response, and failureThreshold how many consecutive failures are needed before Kubernetes reacts. For the liveness probe, a more generous failureThreshold is recommended, since a restart is the most expensive consequence. For the readiness probe, the threshold can be lower, because removal from rotation is much cheaper and quickly reversible.
Another important parameter is initialDelaySeconds, which without a startup probe would need to be manually set to the expected boot time. With a preceding startup probe, this value can be set to zero for liveness and readiness, because the startup probe has already ensured the container finished booting before the regular probes even start counting. This combination significantly reduces guesswork when choosing delay values.
#!/usr/bin/env bash
# check-probes.sh — verify liveness and readiness endpoints locally
# before rolling the manifest out to the cluster
set -euo pipefail
BASE_URL="${1:-http://localhost:8080}"
echo "Checking liveness endpoint..."
curl -fsS -m 2 "${BASE_URL}/health/live" | grep -q '"status":"ok"' \
&& echo "[OK] liveness endpoint healthy" \
|| { echo "[FAIL] liveness endpoint unhealthy" >&2; exit 1; }
echo "Checking readiness endpoint..."
curl -fsS -m 3 "${BASE_URL}/health/ready" | grep -q '"database":true' \
&& echo "[OK] readiness endpoint healthy" \
|| { echo "[FAIL] readiness endpoint unhealthy" >&2; exit 1; }
# Inspect actual probe results reported by Kubernetes for a running pod
kubectl describe pod -l app=symfony-app | grep -A 3 "Liveness\|Readiness"
8. Common pitfalls: timeouts, cascades and false alarms
The most common pitfall with a health check for Symfony is a timeout that is too short combined with a PHP FPM pool already working at its capacity limit under load. If every FPM worker is busy with a real request, the health request has to wait in the same queue for a free worker, causing the timeout to be missed even though the application is actually working, just overloaded. A separate, small FPM pool exclusively for health endpoints can mitigate this problem.
A second trap is the cascade: if a health check checks a dependency that itself depends on yet another dependency, for example an API that internally queries a different database, a failure deep in the chain can cause entire rows of independent services to be marked unhealthy. Health checks should therefore check as directly as possible what the respective service itself needs, without passing through transitive dependencies that already have their own probes elsewhere.
9. Liveness, readiness and startup probe compared
The table below summarizes what each probe type is responsible for in a Symfony Kubernetes setup and what consequence a failure has.
| Probe type | Checks | On failure | External dependencies |
|---|---|---|---|
| Liveness probe | Process is still responding at all | Container is killed and restarted | never check |
| Readiness probe | Application can serve traffic meaningfully | Pod is only removed from rotation | check database, cache, queue |
| Startup probe | Application has finished booting | Liveness/readiness do not start yet | not relevant |
Anyone who correctly separates these three probe types avoids the two most common operational problems in Symfony Kubernetes setups: unnecessary restarts caused by a health check that is too strict, and sluggishly starting pods stuck in a restart loop. The effort for this clean separation is small, while the effect on operational stability is considerable.
Mironsoft
Symfony DevOps, Kubernetes operations and observability setup
Health checks that actually protect your Symfony operations?
We build resilient liveness, readiness and startup probes for Symfony in Kubernetes, including a dedicated health endpoint and cleanly separated dependency checks.
Probe audit
Reviewing existing Kubernetes manifests for health check misconfigurations
Endpoint implementation
Building a dedicated health controller with database, cache and queue checks
Observability
Integrating probe results into monitoring and alerting
10. Summary
A reliable health check for Symfony in Kubernetes consistently separates three questions: is the process still alive, can the application currently serve traffic, and has the container even finished starting. The liveness probe stays deliberately minimal and never checks external dependencies, so a database hiccup does not turn into a restart cascade. The readiness probe checks exactly those dependencies and gently removes the pod from rotation when needed, without killing it.
The startup probe protects slow starting Symfony applications from getting stuck in an endless restart loop. A dedicated endpoint, exempt from authentication, with tight timeouts per dependency, rounds off a robust health check setup. Anyone who maintains this separation from the start saves themselves nighttime alerts caused by restart cascades that were actually triggered by nothing more than a brief, harmless network hiccup.
Symfony Health Checks in Kubernetes — The Essentials at a Glance
Liveness probe
Only check the process state. Never external dependencies, or unnecessary restart cascades threaten.
Readiness probe
Check database, cache and queue. Failure only removes the pod from rotation, no restart.
Startup probe
Mandatory for slow starting applications. Prevents restart loops during the boot process.
Dedicated endpoint
No authentication, tight timeouts per dependency, separated for liveness and readiness.