BadTimeout
BadTimeouts
BadTimeout is the general timeout code: something did not answer in time. It is usually raised by your own stack rather than returned by the server, so the first question is which timer fired - a per-request timer, the connect timer, the session timeout or the subscription keep-alive - and only then why.
What you actually see
Operations fail after a fixed interval that matches an SDK default, and the connection often recovers by itself a moment later, giving a sawtooth of failures and reconnects.
Why it happens
- A client-side timer fired with no response from the serverThe stack gave up waiting. The response may have been lost with the connection, may still be in flight, or may never have been sent. Unlike BadRequestTimeout, which points at a specific request budget, this code often carries no information about where the time went.
- Session timeout expired because nothing kept it aliveThe server returns a RevisedSessionTimeout in the CreateSession response that can be far lower than what you asked for, and many clients ignore it. With no Publish outstanding and no periodic read, the session ages out. A practical keep-alive interval is about a third of the revised timeout, so two consecutive losses are tolerated.
- The client event loop or thread is blockedSynchronous file, database or HTTP work inside a monitored-item callback starves protocol housekeeping. The response arrives on time and is processed late, so the timer fires anyway. This is the most common cause in Python and Node.js collectors and it also delays channel renewal.
- Connect timeout during a handshake stormSession establishment is CPU-bound on the RSA handshake and certificate chain validation. When many clients dial at once, handshakes queue behind cryptography and time out, and the retries deepen the queue - a positive-feedback loop that also produces BadServerTooBusy and orphaned sessions.
- A silently dropping pathA firewall that drops rather than rejects produces timeouts instead of refusals. The distinguishing test is whether a plain TCP connect succeeds while only OPC UA traffic stalls, or whether the socket itself never opens at all.
How to fix it
In diagnostic order: work down, do not skip ahead.
- Find out which timer firedTurn on stack-level tracing and note the elapsed time. A round number matching an SDK default - 5, 10 or 60 seconds - identifies a client timer. A failure at the session boundary points at session lifetime instead.
- Log and honour the revised session timeoutRecord RevisedSessionTimeout from the CreateSession response, not the value you requested, and derive your keep-alive from it. Keep at least one Publish outstanding, or read Server_ServerStatus_CurrentTime periodically, at an interval comfortably shorter than the revised value.
- Get application work off the protocol threadHand notification data to a queue and process it elsewhere. In asyncua and node-opcua, a blocking call inside a callback delays keep-alive, channel renewal and response handling together, which is why these faults arrive as a cluster.
- Stagger connects and back off with jitterAdd a randomised startup delay and bounded exponential backoff to every client. Staggering connects is the most effective client-side measure against connect storms and removes most timeouts seen at plant startup.
- Reconnect properly rather than retrying into the darkOnce a timeout has escalated to a lost session, create and activate a new session, then call TransferSubscriptions with the previous subscription ids and treat GoodSubscriptionTransferred as success. Only rebuild subscriptions and monitored items if the transfer is refused.
Where it shows up, by tool and stack
| Tool / stack | What to do there |
|---|---|
| UA-.NETStandard (C#) | OperationTimeout governs service calls while the Session keep-alive timer is separate. Subscribe to Session.KeepAlive and act on the first missed keep-alive rather than waiting for the fault to surface in application code. |
| asyncua (Python) | Client.session_timeout is what you request and the server's revised value governs. The internal keep-alive is an asyncio task, so a blocked loop delays it and manufactures timeouts on an otherwise healthy link. |
| node-opcua | connectionStrategy controls connect retries and backoff, and the session keep-alive surfaces as keepalive and keepalive_failure events. Tune the strategy instead of writing your own retry loop. |
| open62541 (C) | UA_Client_run_iterate drives keep-alive, channel renewal and the state machine together. A client that only calls services on demand and never iterates will time out and then lose its session. |
| Ignition | The device connection cycles Faulted then Connecting. A regular cadence of these cycles with no network events behind them almost always means session timeout and keep-alive are mismatched. |
| OPC Publisher | Timeouts show up as telemetry gaps and rising reconnect counts rather than obvious errors. Watch the diagnostics output after a restart, when every endpoint dials at once. |
What it looks like in the log
BadTimeout: operation did not complete within 10000 ms
CreateSession requested timeout 600000 ms; server revised to 30000 ms
Keep-alive missed twice; session considered lost, reconnecting and attempting TransferSubscriptions
Confirm the fix
Leave the client idle for longer than the revised session timeout and confirm it stays connected, then run a full plant restart with staggered startup and confirm every client connects within its first two attempts.
Stop it happening again
Derive keep-alive from the revised session timeout everywhere, keep protocol threads free of application work, mandate backoff with jitter in client standards, and reconnect through TransferSubscriptions rather than rebuilding subscriptions.
Stuck on more than one of these? The troubleshooting clinic works through broken configurations end to end, on camera.
Troubleshooting clinic →Last verified 7 September 2026 against OPC UA 1.05.07. OPC UA Connect is an educational resource and is not affiliated with the OPC Foundation.