Skip to main content
🎒 BagROS 2 · June 2026

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.

1

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.

text
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+.

2

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.

bash
# 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.yaml
3

ros2 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.

bash
# 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: volatile
4

ros2 bag info — Inspecting Bags

ros2 bag info shows metadata, topic list with message counts and types, and bag duration.

bash
# 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.

5

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.

bash
# ── 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.

6

Python API — Reading Bags Programmatically

Use rosbag2_py to read bag files in scripts for offline analysis, data export, or processing pipelines.

python
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).

7

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.

yaml
# 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 / ConceptDetails
Record allros2 bag record -a -o bag_name
Record specificros2 bag record /scan /odom /tf -o bag_name
Record regexros2 bag record -e '/(scan|odom)' -o bag_name
Playros2 bag play my_bag/ --rate 0.5 --topics /scan /odom
Play with sim timeros2 bag play my_bag/ --clock (nodes need use_sim_time=true)
Inforos2 bag info my_bag/ (topics, counts, duration, size)
Filter topicsros2 bag filter my_bag/ -o out/ -x '/camera.*'
Convert formatros2 bag convert --input old/ --output-storage mcap --output new/
0 messages recordedQoS mismatch — add --qos-profile-overrides-path with reliability: best_effort
Python readrosbag2_py.SequentialReader() + deserialize_message(data, MsgType)