Symfony Health Checks: Liveness and Readiness Endpoints for Kubernetes
AI generated
SF
{ }
Symfony · Kubernetes · Health Checks · Operations
Symfony Health Checks
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.

15 min read Kubernetes probes Symfony · health checks

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

11. FAQ: Health Checks for Kubernetes: Key Facts

1What's the difference between a liveness probe and a readiness probe?
Liveness only checks whether the process still responds at all and justifies a container restart on failure. Readiness checks whether the application can actually handle traffic and only leads to removal from load balancing on failure.
2Should the liveness probe check the database connection?
No. A database outage is no reason to restart an otherwise functioning PHP process. Such checks belong in the readiness probe instead.
3How is the health endpoint excluded from authentication?
Through a dedicated firewall entry in security.yaml with a matching pattern and security: false, so Kubernetes probes can succeed without sending any credentials.
4How do I check the database connection for readiness?
With a minimal query like SELECT 1 and a short timeout, so a hanging connection doesn't block the entire probe.
5Should every external dependency fail the readiness probe?
Only if it's genuinely required for core application functionality. Secondary features like optional third-party APIs shouldn't block the readiness probe.
6What's a startup probe for, in addition to liveness and readiness?
It suppresses both other probes during a longer but normal boot process, preventing false restarts for slow-starting applications.
7What does the readiness endpoint return during an outage?
An HTTP status 503 with a JSON body indicating which check failed, without exposing sensitive internal details like connection strings.
8How do I add a new health check without touching the controller?
Through a central HealthCheckRegistry that collects check classes automatically via service tagging. A new check class only needs to implement the shared interface.
9Why is too low a failureThreshold on the liveness probe a problem?
Brief load spikes can then get misclassified as an outage and trigger an unnecessary restart, even though the process is fundamentally healthy.
10What happens if liveness and readiness share the same endpoint?
A brief database outage would then count as a liveness failure and trigger unnecessary container restarts, instead of just temporarily pulling the pod out of load balancing.