ROS 2 Bag Guide 2026
Master ros2 bag: record all or selected topics with compression, replay at custom rates with --clock for simulation, inspect with ros2 bag info, filter/convert between MCAP and SQLite3, and read programmatically with rosbag2_py.
MCAP vs SQLite3
ROS 2 Humble and later default to MCAPformat — a single seekable file, faster random access, native Foxglove Studio support, and better compression. If you're still on Foxy/Galactic, SQLite3 (.db3) is the default and plays fine in Humble+. For new projects, MCAP is the right choice.
ros2 bag Overview
ros2 bag records ROS 2 topics to disk and replays them later. The default format is MCAP (from ROS 2 Humble onward) — a structured, seekable container for message streams.
ros2 bag Workflow
═══════════════════════════════════════════════════════
Record → Play back → Inspect → Convert / Filter
Storage formats:
MCAP (.mcap) — Default since ROS 2 Humble (Iron, Jazzy, Rolling)
Seekable, compressed, single file, rosbag2-native
SQLite3 (.db3) — Default in Foxy/Galactic; still supported
One metadata.yaml + one .db3 file per bag
Custom plugins — via storage_plugin parameter
Tools:
ros2 bag record — record topics to a bag
ros2 bag play — replay a bag
ros2 bag info — inspect bag contents
ros2 bag convert — change storage format or reindex
ros2 bag filter — include/exclude specific topics (Humble+)
File layout (MCAP):
my_bag/
├── metadata.yaml — bag metadata (topics, duration, message count)
└── my_bag_0.mcap — message data (single file)
File layout (SQLite3):
my_bag/
├── metadata.yaml
└── my_bag_0.db3⚡ MCAP is preferred for new projects — it supports fast seeking without scanning the whole file, works with Foxglove Studio, and has better compression. SQLite3 bags from ROS 2 Foxy/Galactic still play fine in Humble+.
ros2 bag record
Record all or selected topics to a bag. Use -o to name the output, -e for regex filtering, and --max-bag-size to split large recordings.
# Record ALL topics to auto-named folder (e.g. rosbag2_2026_06_21-14_30_00/)
ros2 bag record -a
# Record specific topics
ros2 bag record /scan /odom /cmd_vel /camera/image_raw
# Record with a custom output name
ros2 bag record -o my_drive_test /scan /odom /tf /tf_static
# Record topics matching a regex pattern
ros2 bag record -e "/(scan|odom|camera)" -o sensor_bag
# Record with MCAP storage (explicit, default in Humble+)
ros2 bag record -a --storage mcap -o my_bag
# Record with compression (reduces file size ~50-80%)
ros2 bag record -a --compression-mode message --compression-format zstd -o compressed_bag
# Record with max bag size (split into multiple files at 1 GB each)
ros2 bag record -a --max-bag-size 1073741824 -o big_session
# Record with max duration (stop after 60 seconds)
ros2 bag record -a --max-bag-duration 60 -o timed_bag
# Record with QoS override (fix BEST_EFFORT publisher to RELIABLE sub)
ros2 bag record /scan --qos-profile-overrides-path qos_overrides.yamlros2 bag play
Replay a bag at real speed or faster/slower. Use --topics to play only selected topics, --loop for continuous replay, and --clock to publish sim time.
# Basic playback
ros2 bag play my_bag/
# Play at 0.5× speed (slow motion)
ros2 bag play my_bag/ --rate 0.5
# Play at 2× speed
ros2 bag play my_bag/ --rate 2.0
# Play only specific topics
ros2 bag play my_bag/ --topics /scan /odom
# Skip the first 30 seconds
ros2 bag play my_bag/ --start-offset 30.0
# Loop continuously
ros2 bag play my_bag/ --loop
# Start paused (press SPACE to step)
ros2 bag play my_bag/ --start-paused
# Publish /clock (for sim time — nodes need use_sim_time=true)
ros2 bag play my_bag/ --clock
# Remap topic on playback
ros2 bag play my_bag/ --remap /old_scan:=/scan
# Play with QoS override (change reliability for subscriber compatibility)
ros2 bag play my_bag/ --qos-profile-overrides-path qos_overrides.yaml
# ── qos_overrides.yaml format ────────────────────────────
# /scan:
# reliability: reliable
# durability: volatileros2 bag info — Inspecting Bags
ros2 bag info shows metadata, topic list with message counts and types, and bag duration.
# Show bag information
ros2 bag info my_bag/
# Example output:
# Files: my_bag_0.mcap
# Bag size: 2.3 GiB
# Storage id: mcap
# Duration: 5m30.123s
# Start: Jun 21 2026 14:30:00.123 (1750000200.123)
# End: Jun 21 2026 14:35:30.246 (1750000530.246)
# Messages: 456789
# Topic information:
# Topic: /scan | Type: sensor_msgs/msg/LaserScan | Count: 3300 | Serialization Format: cdr
# Topic: /odom | Type: nav_msgs/msg/Odometry | Count: 33000 | Serialization Format: cdr
# Topic: /camera/image_raw | Type: sensor_msgs/msg/Image | Count: 9900 | Serialization Format: cdr
# Topic: /tf | Type: tf2_msgs/msg/TFMessage | Count: 330000 | Serialization Format: cdr
# Verbose info (with QoS profiles)
ros2 bag info my_bag/ -v
# Echo a specific message from a bag without playing
# (Humble+ with mcap plugin)
ros2 bag convert --input my_bag/ --output /tmp/slice.mcap --start-time 1750000200 --end-time 1750000210⚡ The message count in ros2 bag info includes /tf which can dominate (TF publishes at 100-500 Hz). Don't be surprised if /tf has 10x more messages than sensor topics.
ros2 bag filter and ros2 bag convert
Extract a subset of topics or time range into a new bag. Convert between MCAP and SQLite3 formats.
# ── ros2 bag filter (Humble+) ─────────────────────────────
# Extract only /scan and /odom into a new bag
ros2 bag filter my_bag/ -o filtered_bag -x "/camera.*" # exclude pattern (regex)
# Keep only specific topics (include pattern)
ros2 bag filter my_bag/ -o sensor_only --include-regex "(scan|odom|tf)"
# ── ros2 bag convert ──────────────────────────────────────
# Convert SQLite3 bag to MCAP
ros2 bag convert --input my_old_bag/ --output my_mcap_bag/
# Convert with explicit storage plugin
ros2 bag convert --input my_bag/ --output-storage mcap --output converted_bag/
# ── Time-slice extraction (MCAP reader) ──────────────────
# Using mcap CLI (install: pip install mcap)
mcap filter my_bag/my_bag_0.mcap --output slice.mcap --start-secs 1750000200 --end-secs 1750000260
# ── Merge bags (concatenate) ─────────────────────────────
ros2 bag convert --input bag1/ --input bag2/ --output merged_bag/⚡ ros2 bag filter with --include-regex keeps only matching topics, --exclude-regex removes matching topics. The regex is matched against the full topic name. Use ^ and $ for exact matches.
Python API — Reading Bags Programmatically
Use rosbag2_py to read bag files in scripts for offline analysis, data export, or processing pipelines.
import rosbag2_py
from rclpy.serialization import deserialize_message
from sensor_msgs.msg import LaserScan
import numpy as np
def read_bag(bag_path: str):
"""Read all LaserScan messages from a bag."""
storage_options = rosbag2_py.StorageOptions(
uri=bag_path,
storage_id="mcap", # or "sqlite3" for .db3 bags
)
converter_options = rosbag2_py.ConverterOptions(
input_serialization_format="cdr",
output_serialization_format="cdr",
)
reader = rosbag2_py.SequentialReader()
reader.open(storage_options, converter_options)
# Filter to specific topics
filter_ = rosbag2_py.StorageFilter(topics=["/scan"])
reader.set_filter(filter_)
topic_types = reader.get_all_topics_and_types()
type_map = {t.name: t.type for t in topic_types}
scans = []
while reader.has_next():
topic, data, timestamp = reader.read_next()
if topic == "/scan":
msg = deserialize_message(data, LaserScan)
ranges = np.array(msg.ranges)
ranges[ranges > msg.range_max] = np.nan
scans.append({
"time": timestamp / 1e9,
"min_range": float(np.nanmin(ranges)),
"mean_range": float(np.nanmean(ranges)),
})
return scans
if __name__ == "__main__":
scans = read_bag("/path/to/my_bag")
print(f"Read {len(scans)} LaserScan messages")
for s in scans[:5]:
print(f" t={s['time']:.3f}s min={s['min_range']:.2f}m")⚡ rosbag2_py is the standard programmatic reader. For Foxglove integration, use mcap-python library. For ROS 1 bag reading from ROS 2, use ros1_bridge or convert first with rosbags-convert (pip).
QoS Override YAML for Record/Play
When publisher and subscriber QoS don't match, bag record may miss messages. Use --qos-profile-overrides-path to fix mismatches.
# qos_overrides.yaml
# Used with: ros2 bag record --qos-profile-overrides-path qos_overrides.yaml
# Override QoS for specific topics
# Problem: publisher uses BEST_EFFORT, but bag subscribe defaults to RELIABLE
# This causes 0 messages recorded for /scan
/scan:
reliability: best_effort # match publisher
durability: volatile
history: keep_last
depth: 10
# Latched topic (TRANSIENT_LOCAL) — for /map or /robot_description
/map:
reliability: reliable
durability: transient_local
history: keep_last
depth: 1
# How to check what QoS a publisher uses:
# ros2 topic info /scan -v
# → QoS: reliability=BEST_EFFORT, durability=VOLATILE
# If messages are missing from the bag, this is the most likely cause.
# Check with: ros2 bag info my_bag/ → message count should be nonzero⚡ Common symptom: ros2 bag info shows 0 messages for a sensor topic. Cause: publisher uses BEST_EFFORT but rosbag2 subscriber defaults to RELIABLE — they're incompatible. Fix: add --qos-profile-overrides-path with reliability: best_effort for that topic.
Quick Reference
| Command / Concept | Details |
|---|---|
| Record all | ros2 bag record -a -o bag_name |
| Record specific | ros2 bag record /scan /odom /tf -o bag_name |
| Record regex | ros2 bag record -e '/(scan|odom)' -o bag_name |
| Play | ros2 bag play my_bag/ --rate 0.5 --topics /scan /odom |
| Play with sim time | ros2 bag play my_bag/ --clock (nodes need use_sim_time=true) |
| Info | ros2 bag info my_bag/ (topics, counts, duration, size) |
| Filter topics | ros2 bag filter my_bag/ -o out/ -x '/camera.*' |
| Convert format | ros2 bag convert --input old/ --output-storage mcap --output new/ |
| 0 messages recorded | QoS mismatch — add --qos-profile-overrides-path with reliability: best_effort |
| Python read | rosbag2_py.SequentialReader() + deserialize_message(data, MsgType) |