PHP SDK guide

Graceful shutdown

A graceful stop prevents new polls, lets the active synchronous task settle, and preserves task completion or failure reporting before the process exits.

Use the managed signal path

Worker::run() installs SIGINT and SIGTERM handlers when pcntl is available. The handler requests shutdown; it does not kill the active PHP call stack.

$worker = new Worker($client, 'orders', buildId: getenv('APP_RELEASE') ?: null);
$worker->registerWorkflow('orders.process', $workflow);
$worker->registerActivity('orders.reserve', $activity);
$worker->run();

Confirm pcntl is present in the production CLI image:

php -r 'exit(extension_loaded("pcntl") ? 0 : 1);'

Match the supervisor grace period

Set the platform's termination grace period longer than the maximum time one synchronous activity can run without reaching a safe boundary. On Kubernetes, combine terminationGracePeriodSeconds with a pre-stop drain only if the platform can address the worker instance reliably.

Long activities should heartbeat progress and split work into idempotent chunks. A PHP signal cannot safely interrupt an arbitrary library call and then claim its side effect did not happen.

Integrate a framework command

Run the managed loop inside your Console command. The supplied Laravel and Symfony worker commands already use this lifecycle:

$worker->run(pollTimeoutSeconds: 1);

The process supervisor delivers SIGTERM; the managed signal path requests shutdown and run() deregisters in its cleanup. A framework callback may call requestShutdown() when it executes during the managed loop. That method sets the shutdown flag; deregistration occurs when run() exits, rather than at the instant the callback runs.

tick() alone does not register a worker, initialize its heartbeat lifecycle or deregister it. A framework-owned while loop around tick() therefore does not provide the managed lifecycle, and requestShutdown() does not supply the missing setup or cleanup. Keep a finite poll timeout with run() so an idle worker can notice shutdown promptly.

Deregistration retries

While a task is claimed, temporary connection failures and proxy HTTP failures such as 502, 503 and 504 retry the original fenced workflow lease renewal or completion request. A completion retry reuses the saved workflow commands or activity result without invoking the handler again. A lost reply may follow a committed outcome, so the Server reconciles the retry using the original task, lease owner and attempt. A revoked or expired claim cannot acquire a new fence through this retry.

These retries use exponential backoff capped at five seconds. Shutdown stops the retry and leaves an unacknowledged outcome for Server recovery. Authentication errors, malformed successful replies and explicit nonretryable Server refusals remain errors. Client start, signal and update calls are not retried by this worker policy.

With a Server that advertises worker_deregistration_fencing, the worker captures its registration token and uses it when deregistering. Temporary connection or database contention can be retried within one ten-second budget covering requests and backoff. A completed receipt is replayed without removing a replacement worker. Any remaining cancellation deadline also limits this budget.

Retries require bounded transport I/O. The default Guzzle transport supports it when ext-curl is installed. Custom unbounded transports make one fenced attempt and emit worker.shutdown_retry_unavailable. Older Servers retain one legacy deregistration attempt. Persistent failures emit worker.shutdown_failed and remain failures. An earlier worker error retains precedence.

Rolling deployment sequence

  1. Start the new compatible worker pool.
  2. Verify registration freshness and expected build ID on every task queue.
  3. Send SIGTERM to the old pool.
  4. Wait for active tasks and registrations to drain.
  5. Force-stop only after the documented grace window.

If a poll returns a typed terminal status such as draining, stopped, or stale registration, the managed worker stops its loop instead of treating that response as idle work.

Shutdown check

Send SIGTERM while the worker is idle and during a bounded test activity. In both cases it should stop within the supervisor window without leaving an unreported completed side effect.