Symfony and Kubernetes: Health Checks with Liveness and Readiness Probes
AI generated
SF
{ }
Symfony · Kubernetes · DevOps · Observability
Symfony and Kubernetes
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.

17 min read Liveness · Readiness · Startup probe · Health endpoint Symfony 7 · Kubernetes 1.30 · PHP 8.4

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.

11. FAQ: Symfony Health Checks in Kubernetes

1Liveness vs. readiness probe?
Liveness decides on restart and checks only the process. Readiness decides on traffic and may include dependencies.
2Does liveness check the database?
No. Otherwise a short database hiccup would restart all pods at once. Database checks belong in the readiness probe.
3When to use a startup probe?
With several seconds to minutes of startup time. Without it Kubernetes interprets slow starts as a failed liveness probe.
4Where does the health endpoint live?
Its own lightweight controller, exempt from authentication, session handling and CSRF protection.
5Which dependencies in readiness?
Database via query, cache via ping, optionally message queue. Each with a tight timeout under one second.
6What happens on readiness failure?
The pod is removed from the service endpoint but not killed. It keeps running until the probe succeeds again.
7Avoiding timeouts under load?
A separate, small FPM pool just for health endpoints, so health requests do not compete with real requests for workers.
8What is a cascade?
Transitive dependencies with their own probes mark entire rows of independent services unhealthy on failure. Check only direct dependencies.
9initialDelaySeconds with a startup probe?
Set it to zero, the startup probe already covers the boot time.
10Single dependency failure = red?
Not necessarily. Depending on how critical the dependency is, internal alerting may be more sensible than removal from rotation.