
Table of Contents
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.
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.
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.
Recommended Probe Configuration
| Probe Type | Parameter | Recommended Value | Rationale |
|---|---|---|---|
| Startup | failureThreshold × periodSeconds | 60–90s total | Allows PHP-FPM + app bootstrap + opcache warmup |
| Readiness | initialDelaySeconds | 5–10s | Wait for startup probe to pass first |
| Readiness | periodSeconds | 5s | Detect dependency failures quickly |
| Liveness | initialDelaySeconds | 0 (use startup probe) | Startup probe gates liveness checks |
| Liveness | failureThreshold | 3 | Avoid restart on transient blips |
| All | timeoutSeconds | 3s | Prevents 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
- 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).
- Check endpoint removal timing: Monitor access logs during deploy. Requests arriving after SIGTERM indicate preStop hook is missing or too short.
- 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.
- 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. - 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.
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.