Both formats answer the same question — where do the messages coming off a robot go? — and in a ROS 2 stack you choose between them with a single --storage flag. The choice matters most later, when someone has to read a two-year-old recording on a machine that does not have your workspace built.
This page compares the two on the axes that actually decide the question for a data-collection program: what survives outside your workspace, how fast you can seek inside a large file, and how each behaves when a recording is interrupted. If you are choosing a training format rather than a recording format, that is a different decision — see choosing a robot data format.
The short version
| MCAP | ROS 2 bag (sqlite3) | |
|---|---|---|
| Container | Purpose-built append-only log | SQLite database file |
| Schemas | Embedded in the file, self-describing | Type names only; definitions must exist locally |
| Random access | Chunk + message index, seek by topic and time | SQL index, whole-file scan for many patterns |
| Compression | Per-chunk zstd or lz4, seeking preserved | None built in |
| Non-ROS readers | Python, C++, Go, Rust, TypeScript, Swift | Practical only via ROS tooling or rosbags |
| Write robustness | Append-only; truncated files still readable | Interrupted writes can leave the DB inconsistent |
| ROS 2 status | Default storage plugin since Iron | Legacy default, still supported |
Where the difference actually bites
Reading a recording you did not make. A sqlite3 bag stores type names, not type definitions. Open one on a machine without the matching message packages and you get topic names and opaque byte blobs. MCAP embeds the schema text in the file, so a reader can decode messages from a package that no longer exists.
Pulling one topic out of a big file. Robot recordings are dominated by camera streams. When you want the 200 Hz joint states out of a 40 GB session, MCAP's per-topic chunk index means the reader touches only the chunks containing that topic. The sqlite3 layout makes the same query considerably more expensive.
Recording that survives a hard stop. Robots lose power mid-session. MCAP's append-only layout means a truncated file still reads up to the last complete chunk. A SQLite file interrupted mid-transaction can require recovery before it opens at all.
Getting data into non-ROS tooling. Training pipelines are Python and PyTorch, not ROS. Reading MCAP from a plain Python process is a pip install mcap mcap-ros2-support away — no sourced workspace, no rclpy. The same property is what lets a recording feed a visualizer like Foxglove and a training job from the same file, rather than maintaining two export paths.
Inspecting a recording without a robot. Because MCAP carries its own schemas, a reviewer can open a session and plot joint states or scrub camera topics without reproducing the robot's software environment. On a data-collection program that runs QA on a different machine than the rig — which is most of them — this is the difference between a five-minute review and an afternoon of dependency archaeology.
Where the sqlite3 bag is still fine
- You are on a pre-Iron distro where MCAP is not the default and you cannot add the plugin.
- Recordings are short, local, and thrown away the same day.
- You genuinely want to query a recording with SQL from an existing tool.
- Existing internal tooling reads .db3 directly and rewriting it is not worth it.
- Recordings are archived, shared, or shipped to a customer.
- Datasets are read by training code outside ROS.
- Files exceed a few GB and you need to seek inside them.
- Message definitions change over the life of the project.
Recording either one
# MCAP (default on Iron and newer)
ros2 bag record -a --storage mcap -o session_014
# Explicit sqlite3, if you need it
ros2 bag record -a --storage sqlite3 -o session_014
# Convert an existing recording
ros2 bag convert -i session_014 -o convert_to_mcap.yamlReading MCAP without ROS on the path:
from mcap_ros2.reader import read_ros2_messages
for msg in read_ros2_messages("session_014.mcap", topics=["/joint_states"]):
print(msg.log_time_ns, msg.ros_msg.position)What neither format gives you
Both are containers, and a container is not a dataset. Neither format decides where an episode starts and ends, whether the operator marked the attempt a success, which protocol version was in force, or whether the cameras were actually in sync when the messages were stamped. That metadata has to be produced by the recording layer and carried alongside the file — see time synchronization for the failure mode that a container cannot detect for you.
Choosing MCAP therefore settles the storage question and none of the pipeline questions. It is still worth settling: the container is the one decision that is expensive to reverse across a corpus of recordings, because reversing it means rewriting every file you have.
The recommendation
Record MCAP. It is the ROS 2 default for a reason, and every property that makes it better — embedded schemas, indexed seeking, per-chunk compression, readable from any language — is a property you only appreciate months later, when the recording has outlived the workspace that produced it. Keep sqlite3 bags for the narrow legacy cases above, and convert anything you intend to keep.