Which OPC UA Timeout Actually Killed Your Session?
The secure channel, the session and the subscription each expire on their own clock. Which one fired tells you to blame the firewall, load or keepalive.
The HMI says "connected." The trend is a flat line. Netstat shows an ESTABLISHED socket to port 4840. That combination trips up a lot of people, because "the port is open" and "the client is receiving fresh data" are two very different claims in OPC UA, and the gap between them is where most session complaints live.
OPC UA stacks three independent lifetimes on top of that TCP socket, and each one expires on its own clock:
- The secure channel (OPC UA Part 4, §5.5). Has a
RevisedLifetime; the client is supposed to renew the token before it runs out. - The session (Part 4, §5.6). Has a
RevisedSessionTimeout. It dies if the server sees no service request from you inside that window. - The subscription (Part 4, §5.13). Has a lifetime counted in publishing intervals, not seconds — and this is the one people misread most.
When data stops, the first job is figuring out which of those three actually expired, because the fixes point in opposite directions. A dead secure channel means clocks or certificates. A dead session means you stopped talking. A dead subscription means publish requests didn't arrive in time. "Just increase the timeout" papers over all three without telling you which one you had.
Read the layer, don't guess it
Before touching a single timeout value, place the failure on a layer. The StatusCode the client logs usually tells you where you are:
| Layer | Where it breaks | Typical StatusCode / symptom |
|---|---|---|
| Network path | Packet loss, duplicate IP, NAT, firewall idle timeout, DNS change | TCP retransmits, no clean close |
| TCP | Connect refused, RST, long retransmission delays | BadConnectionClosed, BadNotConnected |
| Secure channel | Cert trust, security policy mismatch, token renewal, clock skew | BadSecureChannelIdInvalid, BadSecurityChecksFailed |
| Session | Session timeout, user token, server session limit | BadSessionIdInvalid, BadSessionClosed, BadTooManySessions |
| Subscription | Publishing interval, keepalive count, lifetime count | BadSubscriptionIdInvalid, BadNoSubscription |
| Monitored item | Sampling interval, quality, queue overflow | Bad/Uncertain quality, BadMonitoredItemIdInvalid |
A firewall dropping an idle connection and a subscription lifetime expiry both end in "no data," but BadSecureChannelIdInvalid after a quiet period is not the same fault as BadSubscriptionIdInvalid under load. Grab the StatusCode first. It's cheaper than a packet capture and it's usually enough.
The subscription lifetime math people get wrong
Here's the part that bites during commissioning. A subscription's health is governed by three revised numbers the server hands back — request whatever you want, the server clamps it:
- PublishingInterval — how often the server may publish data or a keepalive.
- MaxKeepAliveCount — how many empty intervals pass before the server sends a keepalive. A keepalive is just a Publish response carrying no notifications; it's how the client knows the subscription is alive during quiet periods.
- LifetimeCount — how many publishing intervals with no Publish request from the client before the server deletes the subscription outright.
Part 4 (§5.13.2) is explicit: LifetimeCount should be at least 3 × MaxKeepAliveCount. If you set them close together — say lifetime 10, keepalive 8 — one congested moment where two publish requests arrive late and the server drops your whole subscription. Then the client has to re-create every monitored item, and the operator watches a flat line turn to bad quality and back. I keep lifetime at 3–4× keepalive and let the server revise from there.
The other trap: those are counts, not milliseconds. At a 1000 ms publishing interval, a keepalive count of 10 means a keepalive every 10 seconds — fine for a diagnostic screen. Put that same template on an interlock or command-feedback display and you've built a screen that looks connected while showing status up to 10 seconds stale. Don't copy one subscription template across every tag. Fast operating data, slow diagnostics, and historian collection want different groups and different numbers.
Session keepalive is a separate mechanism
Worth separating from the subscription, because they're often confused. A session dies from RevisedSessionTimeout if the server hears nothing from the client — no Read, no Publish, nothing. Most stacks defend against this with a background keepalive: the UA-.NET client, for example, periodically reads Server_ServerStatus_CurrentTime (NodeId i=2258) on a KeepAliveInterval that defaults to 5000 ms. If that read starts timing out, the client fires a keepalive event before the session actually expires — which is your early warning, not the failure itself.
Request side matters too. Many stacks default RequestedSessionTimeout to 60000 ms, and servers commonly clamp to a floor around 10 s and a ceiling of an hour. Always log the revised value. A client asking for 10 minutes and a server granting 30 seconds is a real and common cause of "random" drops, and you'll chase it forever if you only look at what you requested.
Failure modes that look identical from the HMI
Firewall / VPN idle timeout. Quiet connections get reaped by the NAT or firewall table. The client thinks the session is fine until the next publish times out — long dead period, then a reconnect storm. Tell: the drop interval is suspiciously round. If it's every 30, 60, or 300 minutes on the dot, that's an infrastructure timer, not OPC UA. Check the firewall's idle setting before touching anything in the client.
Server session limit. During commissioning, engineering tools, redundant SCADA nodes, historians, and test clients all pile on. The server hits its MaxSessionCount and starts refusing or evicting. Read Server_ServerDiagnostics — CurrentSessionCount, CumulatedSessionCount, SecurityRejectedSessionCount. The usual culprit is failover tests leaving abandoned sessions that never get cleaned up until the session timeout expires.
Subscription lifetime too short. Covered above — this is the load-related one. Log every subscription-recreated event and line it up against CPU, packet loss, and server diagnostics. If recreates cluster with load spikes, your lifetime-to-keepalive ratio is too tight.
Secure channel token renewal. Renewal fails on clock skew, near-expiry certificates, or a server too busy to service the OpenSecureChannel renew in time. Looks like a random drop. Tell: drops cluster near the secure channel lifetime boundary. Many stacks renew at ~75% of RevisedLifetime, so with a default 3600000 ms channel you'd expect renewal traffic around the 45-minute mark — check NTP and cert validity if drops land there.
Reconnect is a design decision, not a default
A client that hammers a failed endpoint as fast as it can will turn one server restart into a plant-wide connection storm. A reconnect policy worth shipping has:
- A short first retry for brief network bumps, then real backoff after repeated failures.
- Quality set to bad or uncertain while reconnecting — never leave the last good value sitting there looking live.
- Separate reconnect groups for critical vs. noncritical clients where the platform allows it.
- A connection state the operator can actually read, distinguishing disconnected, reconnecting, connected-with-bad-subscription, and connected-with-fresh-data. "Connected" alone is the lie we started with.
If you run redundant servers, test failback, not just failover. A failback that re-creates thousands of monitored items in one burst can hurt more than the outage did. This is also where session transfer earns its keep — a client that can TransferSubscriptions to the backup keeps its subscriptions instead of rebuilding them from scratch.
The one drill to run before handover
Everything above is theory until you've watched it happen once. Before signing off:
- Confirm normal session, subscription, and tag quality as a baseline.
- Pull the client network cable for a known duration; reconnect and time how long until fresh data returns.
- Restart the OPC UA server and record client behavior.
- If the route crosses a firewall or VPN, force an idle period long enough to trip its timer.
- Verify the stale-data and comms-loss alarms actually fire.
- Confirm historian gaps are marked as gaps, not silently interpolated across.
Write the measured recovery time into the project handover. When a real incident hits at 2 a.m., the maintenance engineer needs a number to compare against — "about seven seconds last time we tested" is worth more than any amount of documentation about how reconnect should behave.
And the rule I'd leave on the wall: if increasing a timeout is the only thing that fixed it, you haven't found the fault yet. A longer timeout that hides a firewall reaper, an abandoned-session leak, or a lifetime-to-keepalive ratio set too tight will come back — usually during the one week the person who tuned it is on vacation.