Nodes, topics, and messages: the ROS 2 mental model
Three primitives. Master them and ROS 2 becomes library lookup. Get them wrong and every ROS project feels like fighting the framework.
The first ROS 2 lesson got you a turtle moving. This one gets you the mental model — the three primitives every ROS system is made of. Read this once and 80% of ROS 2 reading comprehension is yours forever.
Node: a process that does one job
A ROS 2 node is just a process. It does one thing — read a camera, drive a motor, plan a path — and communicates with other nodes by sending messages. A typical robot runs 10–50 nodes at once. The camera driver, the odometry estimator, the path planner, the motion controller, each in its own node.
Two crucial consequences of the one-node-per-job rule:
- Crashes are contained. If the path planner segfaults, the motor controller keeps running. A monolithic binary doesn't have this property.
- Languages mix freely. Your perception node can be Python while your real-time controller is C++. They don't know or care.
Topic: a named channel
A topic is a named pub/sub channel. /camera/image_raw carries camera frames. /cmd_vel carries velocity commands. /odom carries odometry. Any node can publish to a topic. Any node can subscribe. No node "owns" a topic — it's just a name with a type attached.
Topics are asynchronous and many-to-many. You can have five publishers on one topic (three sensors fusing into one stream) and three subscribers (logger, planner, safety monitor). No one needs permission.
Message: the typed payload
A message is the data that flows on a topic, defined by a strongly-typed schema. ROS ships a catalog of standard types:
std_msgs/String— a string. Useful for debugging, rarely for production.geometry_msgs/Twist— linear + angular velocity, i.e., "drive forward while turning."geometry_msgs/PoseStamped— a 6-DOF pose with a timestamp and a frame.sensor_msgs/Image— a camera frame with encoding info.sensor_msgs/LaserScan— a lidar sweep.nav_msgs/Odometry— pose + velocity + covariance.
You can define your own (.msg files compile at build time), but start with the standards. Custom types are where ecosystem interop breaks.
Why this scales to real robots
Three primitives might sound too simple to run a real robot. It isn't. The Mars helicopter, the B1 quadruped, and every Waymo prototype run the same three primitives. The reason: the pattern composes.
- Perception pipeline? Camera node → image_proc node → detection node, connected by
sensor_msgs/Imageand a customDetection2DArray. - Navigation? SLAM node publishes
nav_msgs/OccupancyGrid; planner subscribes, publishesnav_msgs/Path; controller subscribes, publishesgeometry_msgs/Twist; base driver subscribes and turns wheels. - Manipulation? Same pattern with
sensor_msgs/JointStatein,trajectory_msgs/JointTrajectoryout.
Every "big" ROS system is this pattern repeated.
Discovery: how nodes find each other
In ROS 1, a central roscore held the directory. ROS 2 eliminated that — discovery is decentralized, using the DDS middleware that ships underneath. Start a node; it announces itself over UDP multicast; other nodes that care subscribe. No master, no single point of failure.
The price: network misconfigurations silently break discovery. In 2026 most people set ROS_LOCALHOST_ONLY=1 during development, then switch to the Zenoh RMW for multi-host deployments.
QoS: the knob that bites every new ROS 2 user
ROS 2 added Quality of Service profiles to topics — reliability, durability, history depth. A publisher and subscriber with incompatible QoS look like they're connected but no data flows. This is the #1 gotcha of ROS 2 in 2026.
Rule of thumb:
- Sensor data (camera, lidar):
rclpy.qos.qos_profile_sensor_data— best-effort, low depth. Drop frames rather than block. - Command data (
cmd_vel, setpoints):rclpy.qos.qos_profile_default— reliable. Don't drop commands. - Latched data (maps, calibration):
durability=TRANSIENT_LOCAL— last value persists for late joiners.
Common mistakes I see in first ROS 2 nodes
- Publishing at 1000 Hz from Python. Python's GIL caps you at ~300 Hz honestly; for high-rate loops use C++ or a RT component.
- Forgetting to call
spin()orspin_once(). Callbacks don't fire unless the executor runs. - Mixing QoS profiles between publisher and subscriber. Symptoms: topic shows up in
ros2 topic listbutros2 topic echois silent. - Using
std_msgs/Stringfor structured data. Define a real message or usejsonin a pinch.
What to do next
Next lesson: write your first publisher and subscriber in Python (and compare the C++ version). That turns this mental model into muscle memory.
Comments
Sign in to post a comment.