Designing a Minimal gz-transport Integration for Gazebo: Boundaries, Checks, and Failure Modes
Most Gazebo integrations only need a standalone gz-transport subscriber, not a custom plugin. This note covers the minimal design, trust boundaries, verification commands, and the failure modes that justify a bigger architecture.
13 Jun 2026, 18:33 UTC

If you are connecting an external process to a Gazebo simulation — a logger, a controller, a test harness — the first decision is not which API to call but whether you need to be inside the simulator at all. Most integrations do not. Gazebo's transport layer, gz-transport, already exposes sensor data, poses, and the simulation clock as Protobuf messages on named topics, and a standalone subscriber process is the smallest design that satisfies most requirements. This note covers when that design is sufficient, where its trust and data boundaries sit, how to verify it is actually working, and the conditions that should push you toward a different architecture.
Requirements that shape the design
Start by classifying what your consumer needs:
- Observation only (logging, metrics, visualization): a subscriber process is sufficient.
- Best-effort command injection (velocity commands, service calls to pause/step): still a client process, publishing or calling services over gz-transport.
- Per-timestep, in-process execution (a controller that must run synchronously with physics): requires a custom system plugin loaded by
gz sim, not a transport client. - Hard real-time determinism: gz-transport does not provide this at all; see the failure-modes section.
The distinction matters because a system plugin couples your code to the simulator's build and lifecycle, while a transport client is an ordinary process you can build, deploy, and restart independently.
The smallest suitable design
For the observation and best-effort-command cases, the minimal architecture has three parts:
- Run the simulator, headless if you do not need rendering:
gz sim -r your_world.sdf(the-rflag starts simulation immediately rather than paused). - In your client, create a
gz::transport::Nodeand subscribe only to the topics you actually consume — for example a pose topic under/world/<world_name>/...or a sensor topic such as a camera or lidar stream. Subscribing to everything "just in case" costs serialization bandwidth and makes debugging harder. - Pin your client against the same gz-transport and gz-msgs versions the simulator was built with. The Protobuf message definitions in gz-msgs are the data contract, and they evolve between Gazebo releases; a client built against a different major version may fail to deserialize or silently misinterpret fields.
Topic names are not fixed by the framework — they come from the world file and plugin configuration. Confirm them against the running simulation before hardcoding anything:
# Run on the same host (or a host with working discovery), no special permissions needed
gz topic -l
gz topic -i -t /world/default/pose/info # message type and publishers
gz topic -e -t /clock # live message streamExpected result: -l lists the topics your world actually advertises, -i shows the Protobuf message type you must code against, and -e prints a continuous stream while simulation is running. If -e prints nothing, the problem is upstream of your client code.
Trust and data boundaries
gz-transport discovers peers with UDP multicast and then exchanges messages over TCP. Two boundaries follow from that:
- Trust boundary: messages are unauthenticated and unencrypted by default. Any process that can join the discovery domain can publish spoofed sensor data or commands, and can subscribe to everything you publish. Treat the network segment as trusted, or do not expose it. On shared or multi-tenant infrastructure, put a secured bridge (for example a DDS-based bridge with security enabled) between the simulation and untrusted consumers rather than exposing gz-transport directly.
- Data boundary: the gz-msgs schemas are the only contract. Version-pin the libraries on both sides, and treat a Gazebo release upgrade as a schema migration that requires re-verifying your client.
Operational checks before blaming your code
When a consumer "isn't getting data," check in this order:
gz topic -lfrom the consumer's environment. If the topic list is empty or incomplete, discovery is broken — this is environmental, not a code bug.gz topic -e -t <topic>to confirm the simulator is actually publishing. A topic can exist with no active publisher if the relevant plugin is not loaded.gz topic -ito confirm the message type matches what your client expects.- If you run with simulated time, subscribe to
/clockand confirm it advances; a paused or stalled clock explains "frozen" downstream behavior.
Failure modes worth planning for
- Multicast discovery fails in containers, CI runners, and across subnets. This is the most common integration failure. Symptoms:
gz topic -lworks on the simulator host but shows nothing elsewhere. Mitigation: setGZ_IPto the correct interface address on each host, or configure explicit/unicast discovery instead of relying on multicast, then re-verify withgz topic -l. - Blocking subscriber callbacks. A slow callback stalls your consumer's message processing, not the simulator. Keep callbacks short and hand work to a queue or worker thread.
- Silent drops on high-rate topics. The transport is best-effort; under load, messages can be dropped without an error. If your consumer needs every sample, that is a design input, not a tuning problem.
- Version skew. Naming changed between Gazebo Classic (
ignition-transport,ignCLI) and modern Gazebo (gz-transport,gzCLI). Confirm which you have withgz sim --versionbefore writing or debugging client code; mixing libraries from different generations does not interoperate.
When to change the design
The minimal client design stops being appropriate when any of these become true: you need deterministic, hard real-time delivery (move the logic into a system plugin or a real-time middleware); the network is untrusted (insert a secured bridge rather than exposing gz-transport); or you scale across hosts where multicast is unavailable (switch to explicit endpoint configuration). Until one of those holds, a pinned-version subscriber process, verified with the gz topic CLI, is the right amount of architecture.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.