tutorial
ROS 2 bag: record, replay, and check the clock
Record six stamped points, inspect their saved contents, and replay them into a ROS 2 node. Reproduce a clock that stays at zero while messages still arrive.
What you will learn
- Record a selected ROS 2 topic and inspect its saved messages.
- Distinguish a message header stamp from the bag's scheduling timestamp.
- Replay the same messages into a fresh subscriber.
- Diagnose an uninitialized simulated clock despite successful delivery.
- Connect the player's clock publication to the consumer's use_sim_time setting.
Before you start
- Topics, publishers, and subscribers
- Compatible ROS 2 communication settings
- A terminal and an installed, running Docker engine
Use ros2 bag record to save a topic, ros2 bag info to inspect the bag, and ros2 bag play to publish its messages again. Replay lets you examine the same input repeatedly. You still need to check the subscriber's clock.
This example records six stamped points in ROS 2 Jazzy. Both replay runs deliver all six unchanged messages. A node with use_sim_time enabled stays at time zero in the first run; adding the player's --clock option supplies the missing time source.
What does a bag let you reproduce?
A bag stores selected ROS messages and information needed to replay them. Stop the source after recording, then let another node consume the saved stream. That gives you a repeatable input for debugging a parser, checking a transform request, or comparing two processing methods.
The official Jazzy recording tutorial introduces this workflow with turtlesim. Here the source emits six known points, so you can check every field and distinguish message delivery from clock behavior.
Run the checked recording and replay
Save bag_workflow.py and verify.sh in the same directory. Open a host terminal there and run:
mkdir -p bag-output
docker run --rm --network none \
--env ROS_DOMAIN_ID=73 \
--env RMW_IMPLEMENTATION=rmw_fastrtps_cpp \
--mount "type=bind,source=$(pwd),target=/lesson,readonly" \
--mount "type=bind,source=$(pwd)/bag-output,target=/output" \
ros:jazzy-ros-base@sha256:066420e07f60aa18262f2479981def87ebcfcec42eefb0c0c57c4a46098348ca \
bash /lesson/verify.sh --output /output/run
Docker pulls the pinned image if needed. Its ROS packages run inside one container with no external network. The script reads the downloaded source files and writes the bag, logs, and report under bag-output/run on your host.
Choose a new run-directory name when repeating the command. The script refuses an existing output directory, so it cannot overwrite an earlier recording. Omitting the output mount and --output /output/run runs the same checks in temporary storage and removes their files afterward.
Validation used ROS 2 Jazzy, Ubuntu 24.04.5, Python 3.12.3, and Fast DDS on ARM64 in Docker Desktop for macOS. The environment record lists exact package versions. The README explains the files and waits; the captured output shows a successful checked run. Other architectures have not been validated here.
The script performs the following steps for you. The commands below explain its subprocesses; they are not extra commands to run in the host terminal.
Record six stamped points
The source publishes geometry_msgs/msg/PointStamped on /bag_demo/point. For index i from 0 through 5, each message contains:
header.frame_id = "map"
header.stamp = 100 + i seconds
point = (0.5i, −0.25i, 0) meters
The first point is (0, 0, 0) at 100 s. The last is (2.5, −1.25, 0) at 105 s. These values describe a supplied teaching sequence; no sensor or localization system produced them.
The source also publishes /clock in that synthetic time domain. The recorder uses --use-sim-time, so its recording timestamps follow the supplied clock. It records only /bag_demo/point; /clock drives the recorder but is absent from the saved topic list.
Inside the container, the script launches this recorder with a dedicated bag directory, shown here as fixture_bag:
ros2 bag record --storage mcap --output fixture_bag \
--use-sim-time --topics /bag_demo/point \
--disable-keyboard-controls
--topics selects the point stream, and --storage mcap selects the installed MCAP storage plugin. --output names a new bag directory. Disabling keyboard controls lets the script manage the recorder through its process handle.
The download waits for discovery before sending the short sequence. With simulated-time recording, initialize /clock before expecting the recorder to subscribe. The Jazzy recorder implementation waits for a clock and uses the recorder's ROS time for each stored message.
Stop recording cleanly so the writer can finish the bag and metadata. The script handles that shutdown and checks the saved result before continuing.
Check the contents before replay
After recording, the script runs:
ros2 bag info fixture_bag
The bag reports MCAP storage, six messages, and one topic of type geometry_msgs/msg/PointStamped. The full command output goes into bag-info.txt. A machine-readable report.json preserves the stored rows and both consumer runs.
For this fixture, the readback checks both header stamps and bag timestamps against [100, 101, 102, 103, 104, 105] seconds. The source holds each clock value while it sends the corresponding point. The script verifies the equality after recording; it does not assume that every bag's two timestamps match.
A six-message total alone cannot establish that the right samples arrived. The Python reader deserializes each stored message and checks its topic, frame ID, coordinates, header stamp, and order. It inspects the bag timestamp separately.
Keep three time values separate
Three time values appear in this workflow:
- Header stamp: a field inside the point message. The publisher assigns its meaning; here it labels the point's supplied acquisition time.
- Bag timestamp: the timestamp stored alongside the serialized message. It supplies the replay schedule. This recorder obtains it from its ROS clock because
--use-sim-timeis active. - Consumer time: the value a receiving node gets from
node.get_clock().now(). A node withuse_sim_time=truefollows received/clockmessages.
A bag timestamp does not generally equal header.stamp. A delayed measurement can carry an earlier acquisition stamp than its recording time. Choosing simulated time for the recorder does not make it extract time from every message header.
The ROS clock design defines zero as an uninitialized ROS clock when simulated time is active and no clock message has arrived. System time follows the computer clock; steady time measures elapsed intervals without calendar-clock jumps. The download uses steady elapsed time for bounded waits so a missing simulated clock cannot freeze its timeout.
Replay the data and the clock
The original publisher exits before replay. Each run creates a fresh subscriber with use_sim_time=true, then starts the player paused. The script waits until the consumer discovers the player and its resume service, allows time for peer discovery, then resumes. The final sequence check detects missed messages.
The first player command omits clock publication:
ros2 bag play fixture_bag --start-paused \
--disable-keyboard-controls --wait-for-all-acked 3000
The second adds it:
ros2 bag play fixture_bag --start-paused \
--disable-keyboard-controls --wait-for-all-acked 3000 --clock 20
The script calls /rosbag2_player/resume after observing the player's topic and service. It also waits for an initial clock in the second run. The acknowledgment wait gives reliable delivery time to drain; the independent receive-count and payload checks determine whether the run passed.
In the first run, all six point messages arrive while the consumer's clock remains zero. Receiving a nonzero header.stamp does not initialize a node's clock. A clock-dependent timer or timestamp comparison can therefore fail even when topic delivery succeeds.
In the second run, --clock 20 tells the player to publish playback time on /clock at 20 Hz. The consumer now receives a time source in the recorded domain. The player's clock option and the subscriber's use_sim_time setting serve different roles; both matter in this example.
The recorded comparison is:
- Without
/clock: six messages, preserved fields, consumer time[0, 0, 0, 0, 0, 0]. - With
--clock 20: six messages, preserved fields, the clock initializes at 100 s while paused, then advances along the replay timeline.
Callback-time clock values can vary between runs because message and clock delivery happen asynchronously. The check verifies the paused clock's initial 100 s value, then checks progression during replay. A callback can observe a clock value slightly beyond the last 105 s header. It need not equal that message's acquisition time.
Neither command rewrites the saved point stamps. The Jazzy player schedules and republishes serialized messages. A playback rate such as --rate 2 changes pacing relative to wall time, while the embedded stamps retain their recorded values.
The player-generated clock follows the bag's timeline. It does not reconstruct an arbitrary source's original clock publication pattern. This bag contains no recorded /clock stream, so the generated clock has one clear authority.
Investigate a replay that looks wrong
Start with the saved data. Confirm the topic, type, count, and payload before changing the subscriber. Then check which clock the subscriber uses.
| Observation | Next check |
|---|---|
| No messages arrive | Check selected topics, matching subscriptions, and QoS compatibility. Start the consumer before resuming the player. |
| Points arrive but simulated time stays zero | Check use_sim_time on that node and whether a /clock publisher actually sends messages. |
| Historical points appear extremely old | Check the time domains before subtracting a header stamp from today's system time. |
| A tf2 lookup fails during replay | Check whether you recorded the required transforms and whether the buffer covers the observation's stamp. |
| Counts differ across runs | Check startup readiness, pauses, topic selection, and processing limits; keep the actual receive counts in the report. |
QoS compatibility still applies to recording and playback. The official override guide explains cases that need explicit profiles. This example uses a compatible local exchange and does not measure packet loss.
The timestamped tf2 tutorial shows why a successful latest-transform lookup can answer the wrong time question. The present bag contains points only, so it cannot populate that tutorial's transform tree.
Decide what the replay establishes
The checks establish that these six supplied messages survive recording and replay, and that the clock configuration changes the subscriber's time source. They do not establish bit-for-bit deterministic execution of an arbitrary robot stack.
Recorded observations remain fixed when a downstream controller changes its action. Use the open-loop and closed-loop simulation lesson when the question depends on how new actions would change future sensor data.
Real workflows also need the relevant transforms, calibration, parameters, message definitions, and software versions. Decide whether to record those dependencies alongside the topic. Replaying into a fresh process helps separate saved input from leftover state; repeated playback inside a running process also requires handling backward time jumps.
For trajectory scoring, continue with the timestamped CSV exercise. It checks associations and coverage explicitly, using a separate synthetic dataset.
For a consumer that estimates motion, continue with robot_localization in ROS 2. That tutorial supplies its own odometry, IMU, and clock streams, then checks a configured EKF against known motion. It does not replay the point-only bag from this lesson.
Try two replay decisions
Exercise 1. A message arrives with header.stamp = 103 s, while a fresh consumer reports ROS time zero. Its use_sim_time parameter is true. What does successful delivery establish, and what should you check next?
Check delivery and clock readiness
Delivery establishes that this message reached the subscription. Its header does not set the node's clock. Inspect the /clock publisher and actual clock messages, then replay with the player's clock option when that is the intended time authority. Do not turn a zero clock into a latency estimate.
Exercise 2. A stored point has header stamp 104 s. You replay the bag at twice normal speed. Does its header become 52 s or 208 s? Should you compare it with today's wall clock to estimate transport delay?
Check playback rate and time domains
The header remains 104 s. Playback rate changes scheduling relative to wall time; it does not rescale the message's fields. Subtracting a synthetic dataset stamp from today's clock mixes time domains. Measure transport delay only after defining compatible clocks and the events each timestamp represents.
Sources and further study
- ROS 2 Jazzy recording and playback tutorial, for the record, inspect, and replay workflow.
- Jazzy rosbag2 recorder, for simulated-time startup and recording timestamps.
- Jazzy playback command and player implementation, for clock publication, playback rate, and serialized-message delivery.
- ROS clock and time design, for system, steady, and ROS time, including uninitialized clocks and time jumps.
- Jazzy recording and playback QoS overrides, for profile compatibility when recording and replaying.