Configuring Deterministic Headless Gazebo Simulations for Automated Integration Testing
Learn how to configure a headless Gazebo server with fixed-step physics and seeded sensor noise so repeated runs produce identical traces for automated integration testing.
25 Sept 2026, 22:00 UTC

Automated integration tests that rely on Gazebo often fail intermittently because the simulation produces slightly different physics and sensor traces on each run. The useful takeaway is that you can make repeated runs reproducible by running the server headless, locking physics to a fixed time step, and seeding the random generators used by sensor noise plugins. This guide walks through that configuration and how to verify it.
Desired Outcome
A headless Gazebo server that produces identical physics and sensor traces across repeated runs, suitable for automated integration testing and continuous integration pipelines. “Headless” means the simulation runs without a graphical user interface (GUI), which removes rendering variability and reduces resource use. “Deterministic” here means byte-for-byte or near-byte-for-byte identical message payloads for the same inputs, within a small tolerance for floating-point comparisons.
Prerequisites
- A supported Gazebo server installation. The GUI is not required and should be disabled.
- An SDF or URDF robot model and a world file that references it.
- A shell on the machine that will run the simulation. No elevated privileges are needed beyond read access to the world and model files.
- A consistent CPU architecture across the machines that run the tests, because floating-point behavior can vary between architectures.
Configure Fixed-Step Physics
Gazebo can advance physics with a variable step size that adapts to wall-clock time, or with a fixed step size that advances the simulation by a constant interval. Variable stepping introduces run-to-run variation because the number of solver iterations depends on how fast the host machine is running. A fixed step removes that dependency.
In your world file, set the physics block to a fixed max_step_size and a real_time_factor of 1.0. The following SDF fragment is representative; verify the exact schema and attribute names for your installed Gazebo version because the physics configuration changed across major releases.
<world name="deterministic_world">
<physics name="fixed_step_physics" type="ode">
<max_step_size>0.001</max_step_size>
<real_time_factor>1.0</real_time_factor>
</physics>
<model name="robot">
<!-- robot model contents -->
</model>
</world>
max_step_size is the simulation time advanced per physics iteration, in seconds. real_time_factor is the target ratio of simulation time to wall-clock time. A value of 1.0 asks the server to keep pace with real time, but with a fixed step the solver cadence remains constant even if the host falls behind.
Seed Sensor Noise Generators
Sensor noise plugins typically draw from a pseudo-random number generator (PRNG). If the generator is unseeded, it may initialize from a wall-clock source or a global engine state, producing different noise sequences on every run. Seeding the generator through the sensor’s SDF parameters makes the sequence repeatable.
Many sensor schemas expose a seed parameter inside the noise block. The exact element name and scope vary by sensor type and plugin version, so confirm the parameter in the plugin documentation for your release. The fragment below shows the pattern; treat the parameter name as a placeholder to verify.
<sensor name="lidar" type="ray">
<!-- sensor configuration -->
<noise type="gaussian">
<mean>0.0</mean>
<stddev>0.01</stddev>
<seed>12345</seed>
</noise>
</sensor>
If your sensor plugin does not expose a seed, or if you are unsure whether the parameter is honored, increase the noise amplitude to a large value temporarily. If identical seeds produce identical sequences across runs, the seeding is working. If the sequences still diverge, the plugin is likely using an unseeded or wall-clock-seeded generator.
Launch the Server Headless
Start the simulation in server-only mode so no rendering or GUI transport is initialized. The exact binary and flags changed across major Gazebo releases; recent versions use gz sim, while older releases may use gzserver. Run the following in a terminal on the simulation host:
gz sim -s -r /path/to/world.sdf
The -s flag starts the server without a GUI, and -r starts the simulation unpaused. Check gz sim --help on your installation to confirm the flags, because the command-line interface has changed between releases. You can also pin the process to a single CPU to reduce scheduling nondeterminism:
taskset -c 2 gz sim -s -r /path/to/world.sdf
taskset is part of the standard util-linux package on Linux. The number 2 is the CPU core index; choose a core that is available on your CI runner.
Expected Checks
After the server starts, confirm that the physics step size and update rate match your configuration. The startup log should report the active step size and update rate. Next, verify that no GUI transport is active by listing the available Gazebo transports:
gz topic -l
Look for the absence of GUI-related topics. Then record the same sensor or odometry topic across two runs and compare the payloads. For example:
gz topic -e -t /odom > run1.log
gz topic -e -t /odom > run2.log
diff run1.log run2.log
If the files are identical, the simulation is deterministic for that topic. If they differ, inspect the first divergence to see whether it originates in physics or sensor noise.
Recovery Options When Traces Diverge
- Disable threaded physics. If your Gazebo build supports a threaded physics option, turn it off. Threaded execution can reorder solver operations in ways that change floating-point accumulation.
- Remove wall-clock-dependent plugins. Any plugin that reads the system clock, uses timers, or spawns asynchronous workers can break determinism. Audit custom and third-party plugins for
std::chronoor ROS time dependencies. - Pin the process to a single CPU. Use
tasksetas shown above. Multi-core execution can vary the order of independent computation. - Verify no rendering components are loaded. Even in server mode, some plugins may attempt to initialize rendering contexts. Check the startup log for warnings about graphics or GUI libraries.
- Eliminate asynchronous network transports. If your test harness subscribes over an unreliable or asynchronous transport, message ordering can vary. Use a reliable, synchronous transport for recorded topics.
Limitations and Version Verification
Determinism in Gazebo is bounded by several factors that you cannot always control:
- Command names, plugin namespaces, and physics schema changed across major releases. Verify the exact server binary and configuration syntax for your installed version before relying on the examples above.
- Floating-point results can differ across CPU architectures, compiler versions, and math libraries. Pin your CI runners to a consistent hardware and software stack.
- Sensor noise seeding behavior varies by sensor type and plugin version. Confirm the parameter name and scope in the plugin documentation for your release.
- Asynchronous network transports and wall-clock-dependent plugins can reintroduce nondeterminism even when physics and noise are seeded.
A practical final check is to run the headless server twice with identical world and model files, record the same sensor topic, and diff the message payloads or checksums. If the payloads match within your tolerance, the configuration is ready for automated integration testing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.