Subscriptions
Subscriptions are the long-poll mechanism for OPC UA — once created, the server pushes notifications until the session ends. This page covers the subscription itself; monitored items, the things being watched, live on their own page.
A subscription is a server-side construct that delivers
notifications to the client at a configured cadence. It carries zero
or more monitored items (variables to sample, events to filter)
and runs an internal publish cycle: every publishingInterval, the
server packages whatever changed and queues a PublishResponse.
The client retrieves those responses by calling publish(). There is
no socket-level "push" — the publish loop is request/response under
the covers — but the model is otherwise the long-poll you'd expect.
Creating a subscription
$sub = $client->createSubscription(publishingInterval: 500.0);
echo "Subscription #{$sub->subscriptionId}, "
. "revised interval: {$sub->revisedPublishingInterval}ms\n";
$publishingInterval
Target sampling rate in milliseconds. The server may revise this — the
actual rate is in $sub->revisedPublishingInterval.
$lifetimeCount
Number of publish-interval ticks the server will keep the subscription alive without a publish request. Default of 2400 × 500 ms = 20 min.
$maxKeepAliveCount
After this many empty ticks, the server emits an empty PublishResponse
to keep the connection warm. Default of 10 × 500 ms = 5 s heartbeat.
$maxNotificationsPerPublish
Cap on notifications per publish reply. 0 = unbounded.
$publishingEnabled
Whether the server should start publishing immediately. Disable to
create a "paused" subscription you enable later with
setPublishingMode().
$priority
Server-side priority hint when multiple subscriptions compete.
SubscriptionResult:
| Field | Meaning |
|---|---|
subscriptionId |
Server-assigned id — use it for every later call |
revisedPublishingInterval |
Actual interval the server agreed to |
revisedLifetimeCount |
Actual lifetime count |
revisedMaxKeepAliveCount |
Actual keep-alive count |
The publish loop
After creating a subscription and adding monitored items, drive the publish loop yourself:
$running = true;
while ($running) {
$publish = $client->publish(acknowledgements: $pendingAcks);
$pendingAcks = [];
foreach ($publish->notifications as $notification) {
// DataChangeReceived events have already been dispatched at this point,
// but you also get the raw notification array here.
handleNotification($notification);
}
if ($publish->sequenceNumber !== 0) {
$pendingAcks[] = [
'subscriptionId' => $publish->subscriptionId,
'sequenceNumber' => $publish->sequenceNumber,
];
}
if ($publish->moreNotifications === false) {
// Server has nothing else queued — back off slightly.
usleep(10_000);
}
}
The publish() call returns a PublishResult carrying:
subscriptionId— the subscription this response belongs tosequenceNumber— to acknowledge in a laterpublish()availableSequenceNumbers— outstanding (un-acked) sequence numbers the server still holdsmoreNotifications—trueif more data is queued server-side; callpublish()again immediatelynotifications— the array of decoded notification payloads
The client also dispatches granular events (DataChangeReceived,
EventNotificationReceived, alarm-typed events). Consumers who prefer
event-driven code can register a PSR-14 listener and ignore the
notification array entirely. See Observability ·
Events.
Acknowledgements
OPC UA requires the client to acknowledge notification sequence numbers
so the server can retire them from its retransmission queue.
Acknowledgements are piggy-backed on the next publish() call:
$acks = [
['subscriptionId' => 12, 'sequenceNumber' => 42],
['subscriptionId' => 12, 'sequenceNumber' => 43],
];
$client->publish(acknowledgements: $acks);
If you fall behind, the server queues until availableSequenceNumbers
reaches its maxNotificationsPerPublish cap or the subscription
expires. To recover lost notifications after a brief outage, see the
republish section below.
Republish
Ask the server for a specific sequence number it still has buffered. Useful when the client missed a publish reply (process restart, brief network blip) but the subscription itself is still alive:
try {
$missed = $client->republish($subId, retransmitSequenceNumber: 41);
process($missed);
} catch (ServiceException $e) {
// BadMessageNotAvailable — the server already discarded it. Tough luck.
}
Deleting
Delete the subscription cleanly when you are done. The server frees all its server-side resources (monitored items, queues, sequence numbers). Leaving subscriptions to expire works but burns server memory until the lifetime count runs out.
Transferring across sessions
When the channel dies and you reconnect(), the new session has no
subscriptions of its own. If the server preserved the subscription
resources (some servers do, some do not), transferSubscriptions()
re-binds them to the new session:
$client->reconnect();
$results = $client->transferSubscriptions(
subscriptionIds: [$oldSubId],
sendInitialValues: true,
);
if ($results[0]->statusCode !== 0) {
// Server lost the subscription too — recreate from scratch.
}
sendInitialValues: true asks the server to re-send the most recent
value of every monitored item, so the client's local cache is fresh.
See Recipes · Recovering from disconnection for the full re-subscription pattern.
What to read next
- Operations · Monitored items — what to put inside a subscription.
- Recipes · Subscribing to data changes — end-to-end pattern, event-driven.