Troubleshooting
Start at the failing boundary and keep the runtime response. A worker process, registered worker, admitted task, durable history event, and terminal result are separate checkpoints.
Triage by symptom
| Symptom | Check first | Likely correction |
|---|---|---|
| Connection path returns 404 | Runtime URL passed to Client |
Remove a manually appended /api; retain a Cloud path prefix. |
| 401 or 403 on start | Control-plane token and namespace | Use the client-role credential for that namespace. |
| Worker cannot register or poll | Worker token, queue, protocol discovery | Use the worker-role credential and a supported runtime. |
| Workflow remains pending | Worker visibility and registered workflow type | Start a compatible worker on the exact task queue. |
| Activity remains scheduled | Registered activity type and capacity | Deploy the handler or split an overloaded queue. |
request_timeout with status zero |
Ordinary control HTTP deadline | Inspect the original operation outcome; a lost response does not authorize repeating a mutation. Configure the HTTP request deadline for the application. |
| Result wait times out | Describe the workflow and inspect history | Distinguish client wait timeout from workflow terminal timeout. |
NonDeterministicWorkflow |
Deployed build and committed command sequence | Restore replay-compatible code or route old histories correctly. |
| Worker exits after a poll | Typed poll status and reason | Treat draining/stopped/stale registration as lifecycle, not empty work. |
Verify each boundary
$health = $client->health();
$cluster = $client->clusterInfo(includeDiagnostics: true);
$workers = $client->listWorkers(taskQueue: 'orders');
$queue = $client->describeTaskQueue('orders');
$execution = $client->describeWorkflow('order-1001');
Keep response status, reason, runtime version, worker protocol, namespace, task queue, workflow ID, run ID, and worker build ID together in an incident record. Remove tokens and sensitive payloads before sharing it.
A start call succeeded, but no work runs
Confirm the client's taskQueue exactly matches the worker's queue. Then confirm the worker registered the exact workflow type string. PHP class autoload success does not prove the protocol type name matches.
If the workflow scheduled an activity, repeat the same check for the activity type. A workflow worker can be healthy while the activity capacity it needs is absent.
Inspect a durable service call
Use an operator credential in the namespace you are investigating. The service catalog read methods return bounded pages and the original durable call, including its deadline, attempts and terminal cause.
$page = $client->listServiceCatalogCalls([
'scope' => 'caller',
'status_bucket' => 'failed',
'per_page' => 50,
]);
foreach ($page['data'] as $row) {
$call = $client->describeServiceCatalogCall($row['id']);
}
Browse registered endpoints, services and operations with
listServiceCatalogEndpoints(), listServiceCatalogServices() and
listServiceCatalogOperations(). Their corresponding
describeServiceCatalogEndpoint(), describeServiceCatalogService() and
describeServiceCatalogOperation() methods use catalog IDs. Lists accept
page and per_page. Filter services by endpoint_id, operations by
endpoint_id or service_id, and calls by those IDs or operation_id.
The methods require the backend's advertised service_catalog_read_contract
version 1. An absent or incompatible capability raises ServerException with
reason: backend_capability_unavailable and the required/advertised contract.
A discovery outage keeps its actual connection or backend failure. It does not
become an unsupported-capability message.
Foreign linked runs remain opaque references. Foreign input, output and failure
payload references are null, with payloads_in_observer_namespace: false.
Authorized payload references remain opaque runtime objects. A visible call
does not authorize browsing another namespace. See the
Server read contract
for scopes, filters, pagination and detail bounds.
For an authorized owner, call detail also includes output_available and the
decoded output. This keeps false, 0, an empty string and null distinct
from a missing result. The original opaque reference remains available.
Downloads use the configured runtime and operator credential, verify size and
digest, and honor the client's maxExternalPayloadBytes limit. Foreign or
unverified output references are not fetched. Decoded values retain the SDK's
typed Avro binary and map adapters.
A result wait timed out
WorkflowHandle::result() polls for a bounded caller duration. When it times out, the durable execution may still be running. Call describe() and inspect its status before starting anything again. Reuse the original workflow ID or an application idempotency key; do not create duplicates just because the client lost patience.
Replay became nondeterministic
Compare the history's scheduled command type and sequence with the deployed workflow handler. Common causes are reordering context calls, renaming an activity type, inserting a timer before an old command, or returning before an operation that existing histories expect.
Restore the compatible implementation for open histories. Validate a fix with a retained replay fixture before redeploying.
Escalate with useful evidence
Include the smallest reproduction, sanitized runtime discovery, the SDK version reported by composer show durable-workflow/sdk, Server version, workflow/run IDs, queue/build identity, and the typed exception or Server reason. Do not include bearer tokens, Cloud secrets, raw customer payloads, or full unredacted history.
Use the generated API Reference for exact constructor and method signatures after the guide identifies the correct concept and failure layer.