Failures & retries
Durable execution does not make every error retryable. Classify transport delivery, activity attempts, deterministic replay, and terminal workflow outcomes separately.
Know which layer failed
| Failure | Meaning | Usual response |
|---|---|---|
TransportException |
The SDK could not complete an HTTP exchange. | Retry only when request identity makes replay safe. |
ServerException |
The runtime rejected or failed an operation. | Inspect status, reason, and details. |
ActivityFailed |
A recorded activity reached a failed terminal attempt. | Catch in workflow code only when compensation is deterministic. |
NonDeterministicWorkflow |
Code no longer matches committed history. | Restore compatible code; do not blindly retry. |
WorkflowFailed |
The execution closed as failed. | Surface the recorded failure to the caller. |
WorkflowTimedOut |
A Server deadline closed the run. | Start a new workflow only if business policy permits. |
WorkflowCancelled |
The run closed as Cancelled, after terminal cancellation or successful cooperative cleanup. | Inspect the cancellation lifecycle and cleanup outcome. |
WorkflowTerminated |
Termination closed the run immediately. | Do not expect workflow-code cleanup after termination. |
Make activity retries safe
Pass activity policy in the command options supported by your runtime, and make the downstream action idempotent:
$receipt = $context->activity(
'payments.capture',
[$orderId, $amount],
[
'start_to_close_timeout_seconds' => 30,
'retry_policy' => [
'maximum_attempts' => 5,
'initial_interval_seconds' => 1,
'maximum_interval_seconds' => 20,
],
],
);
Use the order or activity identity as the payment provider's idempotency key. A retry policy cannot repair a non-idempotent downstream API.
Durable cleanup
Workflow finally blocks can await cleanup activities, timers, and metadata
writes after success or a handled failure. Replay reconstructs those commands
from history. Discarding a suspended replay Fiber does not schedule cleanup or
complete the workflow; PHP may still unwind local finally code, so put external
effects in activities rather than directly in the workflow body.
Use requestCancellation() with cooperating workers when cancellation requires
bounded workflow cleanup. The request preserves one original deadline across
children, activities, duplicates and worker replacement. Shielded durable
cleanup replays after worker loss and successful cleanup closes the run as
Cancelled. See the cooperative cancellation guide
for worker opt-in, operation policies and inspection.
Terminal cancelWorkflow() and terminateWorkflow() still close the run
immediately. They do not resume workflow cleanup, and an already closed run
cannot schedule compensation through finally.
Heartbeat long attempts
foreach ($batches as $index => $batch) {
importBatch($batch);
$context->heartbeat(['completed_batches' => $index + 1]);
}
Heartbeat details make slow work observable and provide a cancellation checkpoint. Catch ActivityCancelled only to release local resources, then rethrow it so the runtime records cancellation correctly.
Cooperative workers supervise callbacks independently of application heartbeats. A stop receipt proves callback stop, while attempt fencing rejects a stale result. Downstream side effects still need application idempotency or reconciliation.
Preserve terminal distinctions at the client
try {
$result = $handle->result(timeoutSeconds: 60);
} catch (WorkflowTimedOut $failure) {
reportDeadline($failure);
} catch (WorkflowFailed $failure) {
reportBusinessFailure($failure);
}
A client-side wait timeout is not proof that the workflow failed; describe the handle or wait again. A workflow terminal timeout is durable Server state.
Treat nondeterminism as a deployment incident
Do not paper over NonDeterministicWorkflow with process retries. Compare the deployed build with the history's worker routing, restore the old command sequence, and roll forward with a replay-compatible change. Keep a replay corpus for long-lived workflow histories in testing.
Before adding a retry, identify the durable request ID, the side effect's idempotency boundary, and what evidence proves the prior attempt did not already succeed.