Timeouts and retry
One timeout knob, one retry knob, and a clear line between recoverable and fatal failures. Wire them deliberately — defaults are conservative, not optimal.
The client exposes two configuration values that govern its tolerance
to slow servers and transient failures: timeout and
auto-retry. Both are set on the builder before connect().
Timeout
ClientBuilder::setTimeout(float \$timeout): self
$timeout
Socket-level read and write timeout, in seconds. Applied to both the
TCP handshake and every subsequent service call. Default: 5.0.
The timeout is a per-syscall watchdog. It does not bound the total duration of a service call — a server that streams a 50 MB browse result in 100 KB chunks, each delivered under the timeout, will keep the connection alive indefinitely. For end-to-end deadlines, use a per-request timer in your application code.
$client = ClientBuilder::create()
->setTimeout(10.0)
->connect('opc.tcp://plc.local:4840');
Picking a value
5.0(default) is appropriate for LAN-attached servers in good health. PLC responses normally land in tens of milliseconds.10.0–30.0for WAN endpoints, busy servers, or servers running on constrained hardware.> 30.0suggests something is wrong with the network path — fix the network rather than raise the timeout further.< 2.0is risky. The OPC UA handshake involves a discovery round-trip plus the OPN/CreateSession/ActivateSession sequence; sub- two-second timeouts will fail spuriously on cold connections.
Auto-retry
ClientBuilder::setAutoRetry(int \$maxRetries): self
$maxRetries
Maximum number of retry attempts per service call. 0 (default)
disables retry — the original exception propagates immediately.
When auto-retry is on, every public service method (read, write,
browse, …) runs inside executeWithRetry. On a recoverable failure
the client calls reconnect() and re-issues the request. Each retry
counts; the total attempt budget is maxRetries + 1 calls (one
original + N retries). After the budget runs out, the last
exception is rethrown.
$client = ClientBuilder::create()
->setAutoRetry(3)
->connect('opc.tcp://plc.local:4840');
$value = $client->read('i=2261'); // up to 4 attempts total
What counts as recoverable
The retry path triggers on transport-level failures and channel/session invalidation:
| Trigger | Exception |
|---|---|
| Socket I/O error or timeout | ConnectionException |
| Channel rejected by the server | ConnectionException (BadSecureChannelClosed) |
| Session invalidated | ServiceException (BadSessionIdInvalid, BadSessionNotActivated) |
These are reasonable to retry because reconnect() clears the state
that caused them. Other exceptions are not retried:
ServiceExceptionwith any other status — the server rejected the request semantically, not transiently. Retrying produces the same rejection.SecurityException— certificate, key, or crypto failures. Almost always configuration bugs.ConfigurationException,EncodingException,InvalidNodeIdException— the call is malformed.ServiceUnsupportedException— the server does not implement the service set. Retrying changes nothing. See Recipes · Handling unsupported services.
Retry events
Each retry attempt dispatches RetryAttempt (with the attempt number,
the operation name, and the triggering exception). When the budget is
exhausted and the last attempt fails, RetryExhausted fires with the
final exception. Wire a PSR-14 dispatcher to surface those — they are
the most useful observability signal you can collect for an unreliable
network path. See Observability · Event
reference.
When the two interact
A 30-second timeout combined with setAutoRetry(3) produces a worst-
case 4×30 = 120-second call. The client does not bound the total
wall-clock duration; tune both knobs to fit your call-site deadline.
Set timeouts and retries based on the type of call site:
- Background workers: generous timeout, retry on
- Web request handlers: tight timeout, retry off (the request budget cannot afford 4×timeouts of latency)
- CLI scripts: generous timeout, retry on
Don't crank both to large values "to be safe". A 60-second timeout
with setAutoRetry(5) means a single broken endpoint can hang your
process for six minutes — long after the operator has retried by hand.
Per-call deadlines
The library does not expose a per-call timeout argument. To enforce a deadline on a specific call without changing the connection-wide setting, wrap the call in a short-lived helper or schedule a SIGALRM:
$deadline = microtime(true) + 2.0;
if (microtime(true) > $deadline) {
throw new RuntimeException('Deadline exceeded before issuing request');
}
$value = $client->read('i=2261');
The pattern above is a soft deadline — it checks the clock around the
call but cannot interrupt a hung socket read. For hard deadlines on
unreliable links, run the client behind
opcua-session-manager,
which adds a daemon-side watchdog.
What to read next
- Recipes · Recovering from disconnection
— re-subscribing after
reconnect(). - Reference · Exceptions — the full classification of recoverable vs fatal errors.