Blog 10 min read
OPC UA sessions vs HTTP API calls, explained for PHP devs
OPC UA sessions vs HTTP API calls, explained for PHP devs
Gianfrancesco Aurecchia
@GianfriAur
If you come to OPC UA from the web, the first read looks reassuringly familiar: point a client at a server, ask for a value, get one back. So it is tempting to treat an OPC UA server like a REST API — connect, read, disconnect, repeat. That instinct is the single biggest source of slow, flaky industrial integrations written in PHP. OPC UA does not do stateless request/response. It does sessions: long-lived, authenticated, stateful connections over an encrypted channel. Here is what that means, and why it changes the PHP you write around it.
TL;DR
HTTP is stateless — every request stands alone. OPC UA is stateful — you open an encrypted secure channel, create a session that carries your identity, and reuse it for many operations. Setup is expensive, so you keep the session warm and reuse it instead of opening one per read. And once it is open, the server can push value changes to you — something a pull-based API simply cannot do.
The HTTP model you already know
REST over HTTP is built on stateless request/response. Each call is self-contained: it carries its own authentication (a bearer token or an API key header), the server answers it without needing to remember the previous one, and any instance behind a load balancer can handle it. The TCP and TLS connection underneath is an implementation detail — it may be reused with keep-alive, but semantically each request is independent.
That model is excellent for the web. It scales horizontally, it fails gracefully, and a single read is cheap and disposable. "Connect, do one thing, forget" is exactly the right shape there. It is also exactly the wrong shape for OPC UA.
What an OPC UA session actually is
OPC UA stacks three things where REST has roughly one. The lifecycle looks like this:
-
01
Open a secure channel
Over the
opc.tcpbinary transport, client and server run anOpenSecureChannelexchange: they agree on a security policy, swap certificates and nonces, and derive the symmetric keys used to sign and encrypt every later message. The secure channel is the protected pipe everything else travels through. -
02
Create and activate a session
On top of that channel the client calls
CreateSession, thenActivateSession, presenting a user identity — anonymous, username and password, or an X.509 certificate. The session is where your identity and per-connection state live. -
03
Use the session
Now you browse the address space, read and write nodes — each addressed by a NodeId such as
ns=2;s=Temperature— call methods, or create subscriptions. The server holds resources for you for the whole life of the session. -
04
Keep it alive
A session has a timeout. The client periodically sends a request to reset that timer; if it lapses, the server discards the session and everything attached to it.
-
05
Close it
When you are done, you close the session and the secure channel so the server can release its resources.
The key word is stateful. The session remembers who you are, what you have subscribed to, and where you were in a long browse. And because the secure channel and the session are deliberately separate layers, a client whose network blips can open a fresh secure channel and re-activate the same session — so subscriptions can survive a reconnect rather than being rebuilt from scratch.

Side by side, the two models barely rhyme:
| HTTP / REST | OPC UA | |
|---|---|---|
| Connection | stateless, per request | stateful, long-lived session |
| Authentication | sent on every request | established once at activation |
| Security | TLS per connection | secure channel: signed + encrypted, keys renewed |
| Data flow | client pulls | client reads or the server pushes |
| Setup cost | negligible | a handshake: channel + session |
| Addressing | a URL path | a NodeId in the server's address space |
| After a drop | just retry the request | reactivate the session; subscriptions transfer |
Why OPC UA works this way
The web fetches documents. OPC UA monitors a live plant. That brings three needs the HTTP model never had to solve.
First, continuous monitoring of many values. Polling thousands of tags through independent requests would hammer both the server and the network. A subscription instead lets the server watch the tags and notify you only when something changes — far less traffic for the same visibility.
Second, strong, established identity and confidentiality. A control system needs to know precisely who is connected and to protect every message. Running a full identity-plus-crypto handshake on every single read would be wasteful; the secure channel and session pay that cost once and reuse it. php-opcua's client speaks this handshake in pure PHP, based on the OPC UA specification, and is tested against UA-.NETStandard, the OPC Foundation's reference implementation.
Third, resilience. Field networks drop. Because the session outlives any one connection, a client can reconnect and carry on without losing its place.
There is also a sessionless mode
OPC UA additionally defines a Pub/Sub model (Part 14) where publishers broadcast data with no client–server session at all — useful for many-to-many telemetry from PLCs and field devices. That is a different tool for a different topology; this article is about the classic client/server session.
Strengths and trade-offs of each model
Neither model is "better" — they solve different problems. The honest comparison:

Stateless HTTP wins when reads are occasional and on demand: it is trivial to scale and cache, every call is cheap and self-contained, and the tooling is everywhere. The price is that the client has to poll for changes, re-authenticates on every call, and keeps no continuity between requests.
An OPC UA session wins when you monitor live data at scale: the server pushes changes instead of being polled, identity and encryption are negotiated once, and the session survives reconnects. The price is the upfront handshake, the server-side state to manage, the keep-alive traffic, and an awkward fit with short-lived web requests — which is exactly what the session daemon further down exists to fix.
Reuse the session — don't reconnect per read
Opening a secure channel means asymmetric cryptography and certificate handling; creating and activating a session is another round trip with its own server-side bookkeeping. All of that happens before your first read. Measured against a single read, the handshake is the expensive part — so the rule is simple: open the session once, reuse it for everything, close it when the process ends.
Build one client per process (or per worker) and reuse it for every read, write and subscription.
Wrap each individual read in its own connect() / disconnect() inside a request handler.
use PhpOpcua\Client\ClientBuilder;
$client = ClientBuilder::create()
->connect('opc.tcp://10.0.0.5:4840');
// one session — reuse it for as many operations as you need
$temperature = $client->read('ns=2;s=Temperature');
$pressure = $client->read('ns=2;s=Pressure');
// ...then, when the process is shutting down:
$client->disconnect();
The client keeps the session alive for you between calls, so the cost of that handshake is amortised across every operation you run on it.
Subscriptions: what a stateless API can't give you
This is the payoff of statefulness. Instead of polling, you tell the server once which nodes you care about and how often it may report; it then pushes a notification whenever a value changes. A monitored item is one watched node; the publishing interval is how often the server may send a batch of changes. The server samples each item and reports only what actually moved.
Conceptually it is two calls on the open session — create a subscription, then attach a monitored item with a callback:
// the server now watches the node and notifies you on change
$subscription = $client->createSubscription(publishingInterval: 1000.0);
$subscription->createMonitoredItem(
nodeId: 'ns=2;s=Temperature',
samplingInterval: 500.0,
callback: fn ($value, $timestamp) => printf("%.1f at %s\n", $value, $timestamp),
);
Check the client documentation for the exact signatures, plus filters and queue options. But notice the real question this raises: that callback only fires while a process is alive to receive pushes. Where does it run in a language built around short requests?
Making sessions work in PHP
Here is the friction. PHP's classic execution model is request-scoped: a web request boots, runs and dies in milliseconds. An OPC UA session wants to live for minutes or hours and be reused, and a subscription needs a process that stays running to receive pushes. The naive "connect in the controller, read, disconnect" pays the full handshake on every page load — and can never receive a subscription callback at all.
The per-request trap
Opening a session inside a web request means every request redoes the secure-channel and session handshake before it can read a single value. It looks fine in a demo and falls over under load.
So the shape of your integration depends on what you need. For occasional reads, keep a session alive outside the request and talk to it, or cache values between reads. For subscriptions and continuous monitoring, run a long-lived worker — a CLI process, a daemon, a queue consumer — not a web request.
The php-opcua ecosystem ships exactly this bridge: a session-manager daemon (built on ReactPHP) that holds OPC UA sessions open in the background while your ordinary PHP requests talk to it over local IPC. Each request reuses an already-open session instead of building one, which drops the per-request connection overhead from roughly 150 ms to about 5 ms. It is transparent, too — if the daemon is running your code gets a managed client, and if it is not you get a direct client, with no code change either way.

Start it with one command:
php artisan opcua:session
php bin/console opcua:session
With the daemon running, the "where does the callback run" problem dissolves: you can define your subscriptions in configuration and receive each data change as a PSR-14 event dispatched into your application's listeners. Keep-alive and reconnection become the daemon's job — your code just consumes the values and events.
So the mental shift is small but decisive. Stop treating an OPC UA server like a REST endpoint. Open a session once and reuse it; for live data, subscribe and let the server push from a long-running process; and let the session daemon reconcile OPC UA's long-lived sessions with PHP's short-lived requests. Get that right and the rest of the integration gets a lot calmer. The client documentation is the place to go next.
OPC UA is a trademark of the OPC Foundation. php-opcua is an independent, community-maintained project and is not affiliated with or endorsed by the OPC Foundation.
Keep reading
OPC UA Subscriptions in Laravel: Build a Live Machine Dashboard
Build a live machine dashboard in Laravel with OPC UA subscriptions: a session-manager daemon streams PLC values into cache while Livewire keeps the UI fresh.
OPC UA in Pure PHP: Introducing the php-opcua Project
php-opcua brings the OPC UA binary protocol to pure PHP: client, CLI and Laravel integration, no C extensions. Read your first PLC value in minutes.