For CI and PID tuning specifically, I have had better luck putting the delay below the middleware rather than inside the graph. A netem qdisc on the interface, or on a veth pair if you only want to shape one direction, gives you delay, jitter with a distribution, reordering and loss in one place, and it never touches your topic list. That is closer to the wire than a delay node, which for a lot of these questions is the same as closer to the simulation.
The reason I moved off delay nodes is not only the topic explosion. A delay node holds an already-delivered message and republishes it later, so what you get is a shifted timestamp. It does not reproduce what the transport does under those conditions. DDS reliability is a heartbeat and NACK conversation, and once you add real latency and loss, retransmit timing, history depth and missed deadlines start driving behavior. Those are usually the things that bite in the field, and a delay node hides all of them.
One caveat that surprises people the first time: netem shapes wall clock. If your control loop runs on /clock and the sim is not real-time, your shaped 100 ms is 100 ms of wall clock, not of sim time. For PID tuning that is usually what you want, but only if you run the sim real-time locked. Worth deciding up front which clock the tuning should be honest to.
Saketh’s point above is the one I would not skip. netem loss and jitter are pseudorandom, so a bare profile gives you a distribution, not a repeatable run, which is useless as a CI gate. Newer kernels let you pin the netem seed, and I keep two profiles: a fixed-delay, zero-jitter, pinned-seed one that gates the build, and a nastier randomized one that runs nightly and is allowed to fail loudly. Recording the injected fault into the run itself, the way he describes, is the more robust version of that, and the two compose: shape at the qdisc, record what you shaped.
For context, I work on remote operations for ROS 2 fleets across links I do not control, so degraded-link behavior is most of what I stare at.