Marcio Cunha

Load Testing Automation for Industrial OPC UA Protocols with Concurrent Go Scripts

Learn how to build concurrent scripts in Go to simulate thousands of sensors connected to OPC UA servers. Uncover the hidden bottlenecks of large-scale industrial telemetry.

Marcio Cunha•5 min
Also available in:EspañolPortuguês
Summary
  • The native concurrency model of the Go language simplifies opening massive TCP connections without exhausting operating system file descriptors.
  • The OPC UA protocol strictly manages sessions and secure channels, requiring careful handshake planning to prevent CPU throttling on the server.
  • Industrial load tests reveal that binary serialization and heavy cryptography often limit performance long before the network bandwidth saturates.
  • Asynchronous metric collection using Go channels prevents the testing system itself from interfering with the measured latency accuracy.
  • Stress validation ensures that industrial plants do not suffer catastrophic supervisory system crashes during peak telemetry operational spikes.

The Reliability Challenge in Industrial Networks

On the factory floor, supervisory systems communicate with thousands of PLCs (Programmable Logic Controllers, which are rugged computers controlling motors, valves, and sensors). This communication requires a common language so that temperature, pressure, and speed data flow without failure. The international standard OPC UA (Open Platform Communications Unified Architecture) acts as a secure universal translator for industry. In practice, it ensures that office software and industrial machinery exchange structured information reliably, even when running different operating systems.

When building or expanding an industrial plant, a critical question arises: how do we know if the server centralizing these data can handle the traffic of ten thousand sensors sending readings every second? Traditional load tests, built for web pages, fail miserably in this scenario. They do not understand the complexity of keeping active binary sessions, managing cryptographic security certificates, and dealing with hierarchical data nodes. This is precisely where software engineering meets hardware, uniting the robustness of industrial protocols with the execution speed of the Go language.

Why the Go Language Excels in Industrial Simulation

The Go language was designed from the ground up to handle multiple concurrent workflows executed simultaneously, a concept known as concurrency. In practice, this means we can create small execution units called goroutines, which consume very little memory compared to traditional threads in operating systems like Linux or Windows. While a standard thread requires megabytes of reserved space, a goroutine starts with just a few kilobytes, easily scaling to tens of thousands of simultaneous instances.

To simulate an entire industrial plant on a single test computer, we need to open independent network connections for each simulated sensor. If we used a heavy language, the load generator itself would crash due to excessive RAM and CPU consumption. With Go, we manage thousands of TCP connections (continuous communication channels between computers) cleanly, using internal channel structures to coordinate when each sensor should read or write data to the OPC UA server.

Architecture of the Concurrent Load Script

Building an efficient load injector in Go requires an architecture based on decoupled producers and consumers. In practice, we divide the script into parts: the first creates a pool of authenticated connections, the second triggers periodic read requests for tags (PLC data points), and the third collects the response time of each operation to generate a reliable statistical report at the end of the process.

Below is a functional snippet in Go demonstrating the basic structure to initialize multiple concurrent clients simulating field devices:

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

func simulateDevice(deviceID int, wg *sync.WaitGroup, results chan<- int64) {
	defer wg.Done()
	start := time.Now()
	// OPC UA handshake and read simulation
	time.Sleep(time.Millisecond * 50)
	duration := time.Since(start).Milliseconds()
	results <- duration
}

func main() {
	numDevices := 1000
	var wg sync.WaitGroup
	results := make(chan int64, numDevices)

	for i := 1; i <= numDevices; i++ {
		wg.Add(1)
		go simulateDevice(i, &wg, results)
	}

	wg.Wait()
	close(results)
	fmt.Println("OPC UA load simulation successfully completed.")
}

This code illustrates the principle of concurrency controlled by goroutines and channels. However, in a real OPC UA environment, each goroutine must instantiate a full client that performs X.509 security certificate exchanges, cryptographic policy negotiation, and secure channel creation before even reading the first process variable.

Hidden Bottlenecks: Cryptography, Memory, and CPU Saturation

When running massive load tests against a real OPC UA server, bottlenecks rarely appear in network bandwidth. The true Achilles' heel is usually CPU usage caused by cryptography and structured data serialization. OPC UA allows different security levels, ranging from completely open connections (rare in production) to high-end cryptography with rigorous digital signatures for every sent packet.

In practice, if we configure five hundred simulated clients to reconnect simultaneously using 2048-bit RSA encryption, the industrial server's processor will operate at its maximum limit within seconds. Another critical point is subscription and monitored item management. Instead of constantly polling a sensor's value, efficient OPC UA uses event-driven notifications. Testing this layer requires the Go script to receive and process thousands of asynchronous messages without blocking the main execution flow.

Measuring Performance and Interpreting Industrial Metrics

Gathering raw data during a load test is useless if we don't know which metrics deserve attention. In automation systems, end-to-end latency (the time it takes for data to leave the PLC, pass through the OPC UA server, and be recorded by the test client) is the most important indicator. Sporadic latency spikes might seem harmless in IT, but in critical industrial processes like controlling a boiler or chemical assembly line, they can trigger false emergency stop alarms.

We recommend monitoring the 99th percentile (p99) of latency instead of the simple arithmetic mean. The p99 shows exactly how long 99% of the fastest requests took, revealing hidden glitches that the average usually hides. Additionally, monitor file descriptor consumption on the operating system where the Go script runs, because Linux systems have default limits that block new network connections if not properly tuned for high scale.

Final Considerations on Load Testing in Critical Environments

Subjecting industrial infrastructures to rigorous load tests with concurrent Go scripts transitions from a luxury to a modern engineering necessity. Industry 4.0 transitions demand that legacy factory floor systems communicate with cloud platforms without losing deterministic stability. Understanding the limits of OPC UA, from certificate negotiation to TCP connection management, protects operations against catastrophic failures during peak production moments.

By adopting lightweight concurrency approaches, engineering teams gain autonomy to validate complex architectures before any equipment is physically installed on the plant floor. Planning, simulating, and analyzing latency metrics with technical rigor ensures that industrial automation delivers safe, predictable, and resilient productivity under any operational stress condition.