OPC UA (Open Platform Communications Unified Architecture) is an industrial communication protocol for exchanging data between devices, PLCs, and SCADA systems. The OPC UA Server is an edge container service that publishes tags on the device so industrial clients can read inference results and write values back.
You define the tags, folders, and server settings from the Deployment Manager Configuration tab, then point your OPC UA client at the device.
OPC UA Server is available exclusively for Enterprise customers. Contact the Roboflow sales team to learn more.
Connection Details
Replace <device-ip> with the IP address shown on the device page in Deployment Manager.
| Purpose | Address |
|---|---|
| OPC UA endpoint | opc.tcp:// |
| Web UI and REST API | http:// |
| Port | Protocol | Purpose |
|---|---|---|
| 4840 | OPC UA Binary | Industrial client connections (PLCs, SCADA) |
| 8092 | HTTP | Web UI, REST API, documentation |
Configuration Source
The "Configuration Source" toggle at the top of the Configure modal controls where the tag and folder configuration comes from. Only one source is ever active on the device at a time.
| Source | Stored in | Use it when |
|---|---|---|
| "Web UI" | OPCUA_CONFIG | You want to define tags, folders, and server settings inline in the modal. This is the default. |
| "File" | OPCUA_CONFIG_FILE | The configuration is too large for an environment variable, such as a deployment with thousands of tags. Point the server at a path on the device (ex: /data/opcua-config.json). |
In "File" mode the server validates at startup that the file exists, is readable, and is not empty, and fails to start with an explicit log entry if any of that is untrue. OPCUA_CONFIG_FILE takes precedence over OPCUA_CONFIG.
Switching sources keeps both the inline values and the file path while the modal is open. On save, the source you did not pick is cleared, so exactly one of the two variables is set. In "File" mode the service card shows the configured file path in place of the inline tag list.
Server Settings
| Setting | Description |
|---|---|
| "Server Name" | Human-readable name for the server instance, shown to clients browsing the network. |
| "Namespace URI" | Unique URI identifying the server namespace that tags are registered under. Defaults to http://opcua.roboflow.run. |
| "Max Sessions" | How many OPC UA clients may be connected at once. Defaults to 200. |
Every client holds one session, including each inference pod writing tags and each SCADA connection. A session belonging to a client that crashed is only released when the server times it out, so a busy site can hold more sessions than it has live clients. Once the cap is reached, further clients are refused with BadTooManySessions and their tags stop updating. Raise the value if a site runs more concurrent clients than the default. Setting it in the modal writes it into the server config, and OPCUA_MAX_SESSIONS can override it per device for hand-managed deployments.
Folders and Tags
Folders organize tags into a hierarchy. Each folder has a name and an optional description, and appears as a node in the OPC UA address space so clients can browse and discover tags. A tag can belong to a folder or sit at the root level.
Tags define the data points the server exposes. Each tag has these fields:
| Field | Description |
|---|---|
| "Display Name" | Human-readable name shown to OPC UA clients. |
| "Browse Name" | Programmatic identifier used in the address space. Generated from the display name. |
| "Data Type" | The type of value the tag holds. |
| "Access Level" | Whether the tag is ReadWrite or ReadOnly. |
| "Folder" | Which folder the tag belongs to, or Root if unassigned. |
| "Initial Value" | Starting value when the server starts. Validated against the data type. |
| "Description" | Optional label describing what the tag is for. |
Data Types
| Type | Description | Example |
|---|---|---|
Boolean | True or false | true |
Int32 | 32-bit signed integer | 42 |
Float | 32-bit floating point | 3.14 |
Double | 64-bit floating point | 3.14159265359 |
String | UTF-8 text | Running |
DateTime | ISO 8601 timestamp | 2024-01-15T10:30:00Z |
These are the types the Configure modal offers. A hand-written configuration file may also use Int16, UInt16, and UInt32, which the server accepts.
Access Levels
ReadWrite tags can be read and written by OPC UA clients as well as the REST API, CLI, and web UI. Use them for control outputs, setpoints, and user-adjustable values.
ReadOnly tags reject OPC UA client writes with BadNotWritable. The REST API, CLI, and web UI can still update the value regardless of access level, so use ReadOnly for sensor values, calculated outputs, and system status that only the device should produce.
Optional Constraints
Numeric types support "Min Value" and "Max Value" to restrict the allowed range, plus "Engineering Units" for a descriptive unit label (ex: °C, PSI, RPM). String types support "Max Length" to cap the character count.
Authentication
The server allows anonymous connections by default, using SecurityPolicy None. Turn on "Require authentication" in the Configure modal to require a username and password from every client.
When the toggle is on you must supply both a username and a password. The password is hashed with bcrypt before it is stored, so the original is never saved and cannot be retrieved later. To change the password on an existing server, enter a new one in the Configure modal, or leave the field blank to keep the current password. Turning the toggle off restores anonymous access.
The credentials are stored as two environment variables, which must be set together or both left unset:
| Variable | Description |
|---|---|
OPCUA_USERNAME | Plain-text username for client authentication. |
OPCUA_PASSWORD_HASH | Bcrypt hash of the password at salt rounds 10, starting with $2b$10$. |
Set credentials through the Configure modal rather than by editing the environment variables. The modal generates the bcrypt hash for you, and setting OPCUA_PASSWORD_HASH by hand requires producing a valid hash externally.
Tag Reporting
The server can report its tags to Roboflow on a schedule, so you can see what it serves without a site visit. Set this in the "Tag Reporting" section of the Configure modal.
| Setting | Description |
|---|---|
| "Send periodic tag reports" | Turns reporting on or off. On by default. |
| "Include tag values" | Sends the current value of each tag. Turn it off to send tag counts only. On by default. |
| "Reporting Interval" | Seconds between reports. Defaults to 60. |
The settings are stored as environment variables, which the server reads at startup:
| Variable | Default | Description |
|---|---|---|
OPCUA_SNAPSHOT_INTERVAL_SECONDS | 60 | Seconds between reports. 0 turns reporting off. |
OPCUA_SNAPSHOT_INCLUDE_VALUES | true | false sends tag counts only. |
OPCUA_SNAPSHOT_TTL_SECONDS | derived | How long a report stays current. The modal derives this from the interval and rewrites it every time you save. |
A value the server does not accept turns reporting off rather than falling back to the default, so a typo stops reports. The device page shows such a device as not reporting.
If any of these variables is set by reference ({$ref}), the modal cannot edit reporting and leaves it as it is on save. Replace the reference with a literal value to manage reporting from the form.
"Include tag values" governs the periodic report only. The server still writes tag value changes to its container log, which Roboflow collects separately.
Monitoring
The device page shows a live OPC UA status card with the connected client count, sessions in use against "Max Sessions", and the latest tag report. A device on an image older than tag reporting says so in place of the report.
When the server hits the session cap and refuses new clients, the card shows a banner with the session count, the cap, and how many clients were refused. To get an email about it, add an "OPC UA Refusing Clients" alert from the device's "Device Alerts" tab and set how many minutes of refusals to allow first. See Set up Device Alerts.
Web Interface
The server hosts a web interface at http://<device-ip>:8092.
| Page | Path | Contents |
|---|---|---|
| Dashboard | / | All configured tags with current values, refreshed every 2 seconds. Any tag's value can be edited through an edit dialog, regardless of access level. |
| Documentation | /docs.html | Server guide, configuration format reference, and troubleshooting notes. |
| API Explorer | /api/docs | Interactive Swagger UI for testing endpoints and viewing request and response schemas. |
HTTP API
The REST API gives programmatic access to tag values and server information. All endpoints return JSON. The base URL is http://<device-ip>:8092/api. See Services for the rules shared by all on-device service APIs.
The HTTP API is unauthenticated. OPCUA_USERNAME and OPCUA_PASSWORD_HASH protect the OPC UA binary endpoint on port 4840, not port 8092. Anything that can reach port 8092 can write any tag.
Tag structure is immutable at runtime. Tags and folders come from the OPCUA_CONFIG environment variable, or the file named by OPCUA_CONFIG_FILE for large configurations, and cannot be created, changed, or deleted through this API. Only values can be written.
Read and Write Tag Values
accessLevel governs the OPC UA protocol, not this API. A tag marked ReadOnly rejects writes from OPC UA clients with BadNotWritable but remains writable here, which is how a pipeline publishes a sensor value that industrial clients should only consume.
curl http://<device-ip>:8092/api/tags
curl -X PUT http://<device-ip>:8092/api/tags/tag_temperature/value \
-H "Content-Type: application/json" \
-d '{"value": 25.5}'List all tags
Returns all configured tags with their current values.
200List of tags with current valuesapplication/json
Show propertiesHide properties
Unique tag identifier
OPC UA Node ID
Human-readable display name
OPC UA data type
BooleanInt16UInt16Int32UInt32FloatDoubleStringDateTimeTag access level
ReadOnlyReadWriteCurrent tag value
Show propertiesHide properties
Timestamp of last value update
Folder name, when the tag is in a folder
GET /tags HTTP/1.1
Host: device-ip:8092
Accept: application/jsoncurl -L \
--request GET \
--url 'http://device-ip:8092/api/tags' \
--header 'Accept: application/json'const response = await fetch("http://device-ip:8092/api/tags", {
method: "GET",
headers: {
"Accept": "application/json"
}
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8092/api/tags"
headers = {
"Accept": "application/json"
}
response = requests.get(url, headers=headers)
print(response.json()){
"tags": [
{
"id": "text",
"nodeId": "text",
"displayName": "text",
"dataType": "Boolean",
"accessLevel": "ReadOnly",
"currentValue": true,
"lastUpdated": "text",
"folder": "text"
}
]
}Get a single tag
Returns a single tag with its current value.
Tag ID
200Tag with current valueapplication/json
Show propertiesHide properties
Unique tag identifier
OPC UA Node ID
Human-readable display name
OPC UA data type
BooleanInt16UInt16Int32UInt32FloatDoubleStringDateTimeTag access level
ReadOnlyReadWriteCurrent tag value
Show propertiesHide properties
Timestamp of last value update
Folder name, when the tag is in a folder
404Tag not foundapplication/json
Error message
GET /tags/{id} HTTP/1.1
Host: device-ip:8092
Accept: application/jsoncurl -L \
--request GET \
--url 'http://device-ip:8092/api/tags/{id}' \
--header 'Accept: application/json'const response = await fetch("http://device-ip:8092/api/tags/{id}", {
method: "GET",
headers: {
"Accept": "application/json"
}
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8092/api/tags/{id}"
headers = {
"Accept": "application/json"
}
response = requests.get(url, headers=headers)
print(response.json()){
"tag": {
"id": "text",
"nodeId": "text",
"displayName": "text",
"dataType": "Boolean",
"accessLevel": "ReadOnly",
"currentValue": true,
"lastUpdated": "text",
"folder": "text"
}
}{
"error": "text"
}Update tag value
Updates the value of a tag. Tags with accessLevel: "ReadOnly" reject writes over the OPC UA protocol but remain writable through this endpoint.
Tag ID
New value to set
Show propertiesHide properties
200Updated tag with new valueapplication/json
Show propertiesHide properties
Unique tag identifier
OPC UA Node ID
Human-readable display name
OPC UA data type
BooleanInt16UInt16Int32UInt32FloatDoubleStringDateTimeTag access level
ReadOnlyReadWriteCurrent tag value
Show propertiesHide properties
Timestamp of last value update
Folder name, when the tag is in a folder
400Missing value, unknown tag, type mismatch, or constraint violation. Note that an unknown tag returns 400 here, not 404. Use GET /tags/{id}, which returns 404, to tell an unknown tag from a bad value. application/json
Error message
PUT /tags/{id}/value HTTP/1.1
Host: device-ip:8092
Content-Type: application/json
Accept: application/json
{
"value": true
}curl -L \
--request PUT \
--url 'http://device-ip:8092/api/tags/{id}/value' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{
"value": true
}'const response = await fetch("http://device-ip:8092/api/tags/{id}/value", {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
"value": true
})
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8092/api/tags/{id}/value"
headers = {
"Content-Type": "application/json",
"Accept": "application/json"
}
payload = {
"value": True
}
response = requests.put(url, headers=headers, json=payload)
print(response.json()){
"tag": {
"id": "text",
"nodeId": "text",
"displayName": "text",
"dataType": "Boolean",
"accessLevel": "ReadOnly",
"currentValue": true,
"lastUpdated": "text",
"folder": "text"
}
}{
"error": "Missing \"value\" in request body"
}A write is rejected with 400 when the value is missing, the type does not match the tag's dataType, or it falls outside a configured minValue, maxValue, or maxLength. Writing to a tag that does not exist also returns 400, not 404. GET /tags/{id} does return 404, so use it if you need to distinguish an unknown tag from a bad value.
Configuration Endpoints
Read-only views of the running configuration, useful for discovering tag IDs and their constraints before writing.
Get full configuration
Returns the complete server configuration, including server settings, folders, and tag definitions. Read-only.
200Full configurationapplication/json
Configuration version
Show propertiesHide properties
OPC UA binary endpoint port
OPC UA namespace URI
Server display name
Maximum concurrent OPC UA sessions. Connections get their own ceiling: the server allows twice this many connections, so a client that reconnects before its old session times out is not refused at the transport layer.
Show propertiesHide properties
Unique folder identifier
Folder display name
Folder description
Show propertiesHide properties
Unique tag identifier
OPC UA Node ID
OPC UA browse name, the programmatic identifier
Human-readable display name
Tag description
OPC UA data type
BooleanInt16UInt16Int32UInt32FloatDoubleStringDateTimeID of the parent folder
Tag access level
ReadOnlyReadWriteInitial value when the server starts
Show propertiesHide properties
Engineering units (ex: degC, kPa)
Minimum allowed value, numeric types only
Maximum allowed value, numeric types only
Maximum string length, String type only
GET /config HTTP/1.1
Host: device-ip:8092
Accept: application/jsoncurl -L \
--request GET \
--url 'http://device-ip:8092/api/config' \
--header 'Accept: application/json'const response = await fetch("http://device-ip:8092/api/config", {
method: "GET",
headers: {
"Accept": "application/json"
}
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8092/api/config"
headers = {
"Accept": "application/json"
}
response = requests.get(url, headers=headers)
print(response.json()){
"version": 1,
"server": {
"port": 1,
"namespace": "text",
"serverName": "text",
"maxSessions": 1
},
"folders": [
{
"id": "text",
"name": "text",
"description": "text"
}
],
"tags": [
{
"id": "text",
"nodeId": "text",
"browseName": "text",
"displayName": "text",
"description": "text",
"dataType": "Boolean",
"folderId": "text",
"accessLevel": "ReadOnly",
"initialValue": true,
"engineeringUnits": "text",
"minValue": 1,
"maxValue": 1,
"maxLength": 1
}
]
}Get all folders
Returns all folder definitions.
200List of foldersapplication/json
Show propertiesHide properties
Unique folder identifier
Folder display name
Folder description
GET /config/folders HTTP/1.1
Host: device-ip:8092
Accept: application/jsoncurl -L \
--request GET \
--url 'http://device-ip:8092/api/config/folders' \
--header 'Accept: application/json'const response = await fetch("http://device-ip:8092/api/config/folders", {
method: "GET",
headers: {
"Accept": "application/json"
}
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8092/api/config/folders"
headers = {
"Accept": "application/json"
}
response = requests.get(url, headers=headers)
print(response.json()){
"folders": [
{
"id": "text",
"name": "text",
"description": "text"
}
]
}Sessions and Health
Once maxSessions is reached, further clients are refused with BadTooManySessions, so compare sessionCount against maxSessions for headroom.
A 200 from /subscriptions with a zeroed session list and maxSessions omitted means the OPC UA server is not running. Treat it as an outage rather than a warm-up: if the endpoint answers at all, startup has already finished or failed. Confirm with /health, which reports status: "unhealthy" and opcuaServer: "stopped".
maxSessions is omitted rather than zeroed in that case so a consumer computing maxSessions - sessionCount does not read an unknown cap as a full one.
List active sessions and subscriptions
Returns the connected OPC UA clients and the tags each one is monitoring. Compare sessionCount against maxSessions for headroom before clients start being refused with BadTooManySessions.
A 200 with a zeroed session list and maxSessions omitted means the OPC UA server is not running. Treat it as an outage rather than a warm-up and confirm with /health. maxSessions is omitted rather than zeroed so a consumer computing maxSessions - sessionCount does not read an unknown cap as a full one.
200Sessions and their subscriptionsapplication/json
Number of active sessions
Configured maximum concurrent sessions, for headroom against sessionCount. Omitted when the OPC UA server is not running.
Total number of subscriptions across all sessions
Show propertiesHide properties
Show propertiesHide properties
GET /subscriptions HTTP/1.1
Host: device-ip:8092
Accept: application/jsoncurl -L \
--request GET \
--url 'http://device-ip:8092/api/subscriptions' \
--header 'Accept: application/json'const response = await fetch("http://device-ip:8092/api/subscriptions", {
method: "GET",
headers: {
"Accept": "application/json"
}
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8092/api/subscriptions"
headers = {
"Accept": "application/json"
}
response = requests.get(url, headers=headers)
print(response.json()){
"sessionCount": 1,
"maxSessions": 1,
"subscriptionCount": 1,
"sessions": [
{
"sessionId": "text",
"sessionName": "text",
"clientDescription": "text",
"connectedAt": "text",
"lastContactTime": "text",
"subscriptionCount": 1,
"subscriptions": [
{
"subscriptionId": 1,
"publishingInterval": 1,
"monitoredItemCount": 1,
"monitoredItems": [
{
"nodeId": "text",
"displayName": "text",
"samplingInterval": 1
}
]
}
]
}
]
}/subscriptions is missing from the spec the device serves at /api/openapi.json. The route is live regardless, and it is documented here.
/health returns 200 whether or not the OPC UA server is running. Check status and opcuaServer rather than the status code.
Health check
Returns the health status of the OPC UA server and API.
200Health status. Returns 200 even when the OPC UA server is stopped, with `status` set to `unhealthy`.application/json
Overall server health status
healthyunhealthyOPC UA server status
runningstoppedServer uptime in seconds
Number of configured tags
Why the OPC UA server is not running. Present only when opcuaServer is "stopped".
GET /health HTTP/1.1
Host: device-ip:8092
Accept: application/jsoncurl -L \
--request GET \
--url 'http://device-ip:8092/api/health' \
--header 'Accept: application/json'const response = await fetch("http://device-ip:8092/api/health", {
method: "GET",
headers: {
"Accept": "application/json"
}
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8092/api/health"
headers = {
"Accept": "application/json"
}
response = requests.get(url, headers=headers)
print(response.json()){
"status": "healthy",
"opcuaServer": "running",
"uptime": 3600,
"tagCount": 5
}CLI
The opcua-cli tool gives terminal access to the server for debugging, scripting, and quick tag operations when the web UI is not reachable.
docker exec -it opcua-server opcua-clidocker exec opcua-server opcua-cli <command>| Command | Description |
|---|---|
list, ls | List all tags with current values |
read | Read a specific tag value |
write | Write a value to a tag |
status | Show server health and uptime |
clients | List connected OPC UA clients |
clients --detailed | List clients with subscription details |
export | Export the configuration as JSON |
docker exec opcua-server opcua-cli read Temperature
docker exec opcua-server opcua-cli write Temperature 25.5
docker exec opcua-server opcua-cli clients --detailedLogging
Set LOG_LEVEL on the service to control verbosity. See Update Device Configuration for where to edit service environment variables, and View Device Logs for reading the output.
| Level | Description |
|---|---|
DEBUG | Detailed diagnostic information |
INFO | General operational messages (default) |
WARN | Potential issues that do not stop operation |
ERROR | Errors that affect functionality |
Values are case-insensitive, and an unrecognized value falls back to INFO.