opcua-client · v4.6.x
Docs · Operations

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

php examples/subscribe.php
$sub = $client->createSubscription(publishingInterval: 500.0);

echo "Subscription #{$sub->subscriptionId}, "
   . "revised interval: {$sub->revisedPublishingInterval}ms\n";
Parameters
$publishingInterval
float optional default 500.0

Target sampling rate in milliseconds. The server may revise this — the actual rate is in $sub->revisedPublishingInterval.

$lifetimeCount
int optional default 2400

Number of publish-interval ticks the server will keep the subscription alive without a publish request. Default of 2400 × 500 ms = 20 min.

$maxKeepAliveCount
int optional default 10

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
int optional default 0

Cap on notifications per publish reply. 0 = unbounded.

$publishingEnabled
bool optional default true

Whether the server should start publishing immediately. Disable to create a "paused" subscription you enable later with setPublishingMode().

$priority
int optional default 0

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:

php examples/publish-loop.php
$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 typed notification objects 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 to
  • sequenceNumber — to acknowledge in a later publish()
  • availableSequenceNumbers — outstanding (un-acked) sequence numbers the server still holds
  • publishTime — when the server sent the notification message
  • acknowledgementResults — the status code of each acknowledgement you passed to this publish(), in order: Good, or e.g. BadSequenceNumberUnknown for a sequence number the server no longer holds and BadSubscriptionIdInvalid for an unknown subscription
  • moreNotificationstrue if more data is queued server-side; call publish() again immediately
  • notifications — an array of DataChangeNotification (clientHandle, dataValue) and EventNotification (clientHandle, eventFields) value objects; distinguish them with instanceof

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:

php ack on the next publish
$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 — one it sent but you have not acknowledged yet. Useful when the client missed a publish reply (process restart, brief network blip) but the subscription itself is still alive. availableSequenceNumbers on the last PublishResult (or on a TransferResult) lists what can still be retransmitted:

php recover a missed notification
use PhpOpcua\Client\Module\Subscription\DataChangeNotification;

try {
    $missed = $client->republish($subId, retransmitSequenceNumber: 41);

    foreach ($missed['notifications'] as $notification) {
        if ($notification instanceof DataChangeNotification) {
            process($notification->clientHandle, $notification->dataValue);
        }
    }
} catch (ServiceException $e) {
    // BadMessageNotAvailable — the server already discarded it.
}

The result is an array with sequenceNumber, publishTime and notifications, which holds the same DataChangeNotification / EventNotification objects publish() returns.

republish() dispatches the same DataChangeReceived, EventNotificationReceived and alarm events as publish(), with republished set to true. A retransmitted message can repeat notifications publish() already delivered — any sequence number you had not acknowledged yet — so a listener that must not process a value twice checks the flag:

php tell retransmissions apart
use PhpOpcua\Client\Event\DataChangeReceived;

$listeners->listen(DataChangeReceived::class, function (DataChangeReceived $e) {
    if ($e->republished && $store->has($e->subscriptionId, $e->sequenceNumber)) {
        return;
    }

    $store->save($e->subscriptionId, $e->sequenceNumber, $e->clientHandle, $e->dataValue);
});

PublishResponseReceived and SubscriptionKeepAlive describe a publish round-trip, so only publish() dispatches them.

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:

php re-bind after reconnect
$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.