Graceful Shutdown and Health Checks in PHP

Khimananda Oli 8 min read Programming and Languages
Graceful Shutdown and Health Checks in PHP

By Khimananda Oli | Last reviewed: August 2026

Dropped requests during deployments remain one of the most common reliability failures I see in PHP environments, even in 2026. The root cause is usually a mismatch between how orchestrators send termination signals and how PHP-FPM or Laravel actually handles them. Implementing graceful shutdown and health checks in PHP requires coordinating OS-level signal handling, application lifecycle hooks, and infrastructure probes to ensure traffic only flows to instances that are truly ready and willing to serve.

Load BalancerStops New TrafficPHP ContainerHealth Check EndpointSIGTERM HandlerActive Request DrainDatabase / CacheDependency CheckShutdown Complete → Exit 0
Graceful shutdown and health checks in PHP coordinate traffic draining, dependency verification, and signal handling across the stack.

How do you configure PHP-FPM for graceful shutdown?

PHP-FPM does not gracefully shut down by default in many older configurations; it simply terminates worker processes when receiving SIGTERM, potentially cutting off active requests. Modern PHP-FPM (8.2+) handles SIGTERM more predictably, but you must still configure timeouts explicitly. The key directive is process_control_timeout, which tells the master process how long to wait for workers to finish current requests before forcing termination.

Essential PHP-FPM Pool Configuration

In your pool configuration file (typically /etc/php/8.4/fpm/pool.d/www.conf), set these directives to enable proper request draining:

; Allow workers to finish current requests
process_control_timeout = 30s

; Prevent new requests from being accepted during shutdown
; This is implicit when SIGTERM is received, but verify your version

; Ensure slow logs capture stuck requests during drain
request_slowlog_timeout = 25s
slowlog = /var/log/php-fpm/slow.log

; Maximum execution time should be LESS than process_control_timeout
max_execution_time = 25

A common mistake is setting process_control_timeout higher than your orchestrator's terminationGracePeriodSeconds. If Kubernetes force-kills the pod at 30 seconds but PHP-FPM expects 45 seconds to drain, you will still drop requests. Always align these values with a buffer. For teams managing complex database connections alongside PHP, understanding MySQL performance tuning helps ensure queries complete within your drain window rather than hanging indefinitely.

Handling Signals in Custom PHP Workers

If you run long-lived PHP processes (queue workers, WebSocket servers via RoadRunner or Swoole), you must trap signals manually. PHP's pcntl_signal function allows you to intercept SIGTERM and initiate a controlled shutdown:

<?php
$running = true;

pcntl_async_signals(true);
pcntl_signal(SIGTERM, function () use (&$running) {
    error_log('[Worker] Received SIGTERM, finishing current job...');
    $running = false;
});

while ($running) {
    $job = getNextJob(); // Blocks or polls
    if ($job) {
        processJob($job);
    }
}

error_log('[Worker] Drained. Exiting cleanly.');
exit(0);

This pattern ensures the worker completes its current unit of work before exiting. Without it, SIGTERM kills the process mid-job, causing duplicate processing or data corruption. For queue-heavy Laravel applications, this aligns with best practices covered in Laravel queues and jobs background processing.

What makes a reliable PHP health check endpoint?

A health check that only returns HTTP 200 is worse than useless—it creates false confidence. A production-grade health endpoint must verify that the application can actually perform useful work. This means checking downstream dependencies, not just confirming the web server is running. When implementing graceful shutdown and health checks in PHP, distinguish between liveness (is the process alive?) and readiness (can it serve traffic?).

Liveness vs. Readiness Probes

  • Liveness: Returns 200 if the PHP process is running and responsive. No external dependencies. Used to detect deadlocks or frozen processes. Failure triggers a container restart.
  • Readiness: Returns 200 only if all critical dependencies (database, cache, external APIs) are reachable. Failure removes the pod from service load balancers without restarting it.

Implementation Example in Plain PHP

Create a dedicated /health/ready endpoint that performs real checks with strict timeouts:

<?php
header('Content-Type: application/json');

$checks = [];
$healthy = true;

// Database check with 2-second timeout
try {
    $pdo = new PDO(
        getenv('DATABASE_URL'),
        null, null,
        [PDO::ATTR_TIMEOUT => 2]
    );
    $pdo->query('SELECT 1');
    $checks['database'] = 'ok';
} catch (Throwable $e) {
    $checks['database'] = 'fail: ' . $e->getMessage();
    $healthy = false;
}

// Redis check
$redis = new Redis();
try {
    $redis->connect(getenv('REDIS_HOST'), 6379, 2.0);
    $redis->ping();
    $checks['cache'] = 'ok';
} catch (Throwable $e) {
    $checks['cache'] = 'fail: ' . $e->getMessage();
    $healthy = false;
}

http_response_code($healthy ? 200 : 503);
echo json_encode([
    'status' => $healthy ? 'ready' : 'unavailable',
    'checks' => $checks,
    'timestamp' => date('c')
], JSON_PRETTY_PRINT);

Never let health checks take longer than 3–5 seconds total. Orchestrators have their own timeouts, and a slow health check looks identical to a failed one. Also, avoid caching health check results—each probe must reflect real-time state. For deeper observability integration, pair these endpoints with OpenTelemetry instrumentation to correlate health failures with traces and metrics.

KubernetesPHP-FPMReadiness ProbeLoad BalancerSIGTERM SentReturns 503 ImmediatelyRemoved from PoolDraining ActiveRequests(max 30s)Exit 0 (Clean)Timeline: SIGTERM → 503 → Drain → Exit
Correct probe timing ensures PHP-FPM drains requests before Kubernetes terminates the pod during graceful shutdown.

How do you tune Kubernetes probes for PHP applications?

Default Kubernetes probe settings are rarely appropriate for PHP. PHP-FPM has non-trivial startup time (loading opcache, warming config caches, establishing persistent connections), and shutdown requires the drain period discussed above. Misconfigured probes cause CrashLoopBackOff during startup or dropped requests during termination.

Probe TypeParameterRecommended ValueRationale
StartupfailureThreshold × periodSeconds60–90s totalAllows PHP-FPM + app bootstrap + opcache warmup
ReadinessinitialDelaySeconds5–10sWait for startup probe to pass first
ReadinessperiodSeconds5sDetect dependency failures quickly
LivenessinitialDelaySeconds0 (use startup probe)Startup probe gates liveness checks
LivenessfailureThreshold3Avoid restart on transient blips
AlltimeoutSeconds3sPrevents probe itself from blocking

Critical Alignment Rule

Your terminationGracePeriodSeconds must exceed your process_control_timeout by at least 5–10 seconds. This buffer accounts for preStop hook execution and kernel cleanup. If PHP-FPM needs 30 seconds to drain, set terminationGracePeriodSeconds: 40. Without this margin, Kubernetes sends SIGKILL while PHP-FPM is still draining, defeating the entire purpose of graceful shutdown and health checks in PHP.

# Kubernetes Deployment snippet
spec:
  terminationGracePeriodSeconds: 40
  containers:
  - name: php-fpm
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 5"]
    readinessProbe:
      httpGet:
        path: /health/ready
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5
      timeoutSeconds: 3
      failureThreshold: 3
    livenessProbe:
      httpGet:
        path: /health/live
        port: 8080
      periodSeconds: 10
      timeoutSeconds: 3
      failureThreshold: 3
    startupProbe:
      httpGet:
        path: /health/live
        port: 8080
      periodSeconds: 5
      failureThreshold: 18

The preStop sleep is deliberate: it gives the kube-proxy/iptables rules time to propagate the endpoint removal before PHP-FPM stops accepting connections. Without it, there is a race condition where new requests arrive after SIGTERM but before the load balancer updates its routing table. Teams deploying on AWS EKS or similar managed platforms should also review Amazon EKS networking specifics, as VPC CNI plugins have different propagation delays than kubenet.

Why are my PHP containers still dropping requests during deploys?

If you have implemented everything above and still see errors, the issue typically lies in one of three areas: missing preStop hooks, dependency health checks that are too lenient, or application code that ignores shutdown signals.

Debugging Checklist

  1. Verify signal receipt: Add logging to your PHP shutdown handler or check PHP-FPM logs for "exiting gracefully" messages. Absence confirms SIGTERM never reaches the process (often a PID 1 issue in Docker).
  2. Check endpoint removal timing: Monitor access logs during deploy. Requests arriving after SIGTERM indicate preStop hook is missing or too short.
  3. Validate dependency timeouts: A health check that waits 10 seconds for a dead database will timeout the probe before reporting failure. Set hard timeouts on every dependency call inside health endpoints.
  4. Inspect long-running requests: If any request exceeds process_control_timeout, it will be killed. Identify these via slow logs and either optimize them or increase drain time.
  5. Confirm exit code: PHP-FPM must exit with code 0 after draining. Non-zero exits signal failure to the orchestrator, which may trigger alerts or affect deployment rollout strategies.
❌ Broken ShutdownSIGTERMRequest Mid-FlightSIGKILLConnection Reset by PeerNo preStop • No Drain • Hard Kill✓ Correct ShutdownSIGTERM503 + DrainFinish ReqExit 0All Requests CompletedpreStop Sleep • Health 503 • Full DrainKey Differences✗ Immediate termination✓ Controlled drain window✗ Dropped client connections✓ Zero request loss✗ False-positive health during shutdown✓ Immediate 503 on SIGTERM✗ Orchestrator fights application✓ Aligned grace periodsGraceful shutdown and health checks in PHP
Broken versus correct graceful shutdown behavior in PHP: timing alignment prevents dropped requests and connection resets.

Conclusion

Reliable PHP deployments depend on treating graceful shutdown and health checks in PHP as a unified system, not isolated configurations. Align your PHP-FPM drain timeouts with Kubernetes termination grace periods, expose meaningful readiness endpoints that verify real dependencies, and always include a preStop hook to allow network propagation. Test your shutdown behavior under load—not just in staging with idle containers—because race conditions only surface when requests are actually in flight. If your team needs help auditing your PHP deployment pipeline or designing compliance-ready infrastructure that survives audits and traffic spikes alike, reach out to discuss your architecture.

Frequently Asked Questions

Configure process_control_timeout in your php-fpm.conf to allow workers time to finish requests before termination. Set this value higher than your longest expected request duration, typically 30 seconds, ensuring active connections complete without dropping during deployments or restarts in 2026 production environments.

Liveness checks if the PHP process is running and responsive, triggering restarts on failure. Readiness verifies dependencies like databases are accessible before accepting traffic. Both are essential for Kubernetes orchestration but serve distinct purposes in maintaining application availability during scaling events.

Yes. Octane uses Swoole or RoadRunner which maintain persistent worker processes requiring explicit signal handling. Configure max_execution_time and listen for SIGTERM signals in your server configuration to drain active requests properly before workers terminate during deployment cycles.

Send SIGTERM to your PHP-FPM master process using kill -15 command while running concurrent requests with Apache Bench or wrk. Monitor access logs to verify zero 502 errors and confirm all in-flight requests complete successfully before the process exits cleanly.

Create a dedicated /health route returning 200 OK with minimal overhead. Include database and cache connectivity checks for readiness probes. Avoid heavy computations or external API calls that could timeout during infrastructure validation cycles in containerized environments.

Load balancers continue routing traffic to terminating containers lacking proper shutdown hooks. Implement preStop lifecycle hooks in Kubernetes allowing five seconds for connection draining before SIGTERM reaches PHP-FPM, preventing premature request interruption during rolling updates.

Absolutely. Install pcntl_async_signals and register handlers for SIGTERM and SIGINT to set shutdown flags. Check these flags between job iterations in queue workers, enabling clean exit after current task completion rather than mid-execution interruption.

Set initialDelaySeconds to 10 and periodSeconds to 15 in Kubernetes. Configure probe timeouts under 3 seconds since health endpoints should respond instantly. Longer timeouts mask underlying performance issues and delay failure detection during incident response scenarios.

Ensure master process receives signals correctly by avoiding PID 1 in containers without init systems. Use tini or dumb-init as entrypoint to reap orphaned workers. Verify process tree cleanup with ps aux commands after sending termination signals.

Bypass authentication entirely for infrastructure probes. Health endpoints must remain accessible without tokens or sessions since load balancers cannot provide credentials. Restrict access via network policies instead, allowing only internal cluster IPs to query readiness and liveness paths.

Blocking operations like synchronous file writes or unbuffered database queries prevent signal processing. Enable pcntl_async_signals in PHP 8.4+ or restructure long-running tasks into smaller chunks checking shutdown flags periodically between operations.

Stale opcache may serve outdated health endpoint code after deployments. Call opcache_reset() in deployment scripts or configure opcache.revalidate_freq appropriately. Validate cache invalidation works by checking response headers confirming new code version serves health responses.

Yes. Attackers enumerate infrastructure details through verbose health responses. Return generic 200 OK publicly while exposing detailed dependency status only on internal ports or authenticated admin routes. Never leak database versions, memory usage, or stack traces externally.

Track request error rates and p99 latency spikes during deployments using Prometheus metrics. Alert on increased 5xx responses correlating with pod terminations. Establish baseline shutdown duration metrics to detect regressions when configuration changes impact request draining behavior.

Match your SLA maximum request duration plus buffer. For APIs averaging 2 seconds with occasional 10-second uploads, set 15 seconds. Excessive values delay scaling; insufficient values cause dropped requests. Profile actual request distributions before finalizing this critical configuration parameter.