Skip to main content
🔍 IntrospectionROS 2 · June 2026

ROS 2 Node Graph & Introspection Guide 2026

Master ROS 2 debugging tools: ros2 node/topic/service/action CLI, rqt_graph, namespace remapping, programmatic graph API, ros2 doctor, and tf2 frame debugging.

The ROS 2 Computation Graph

The ROS 2 computation graph is the live network of nodes, topics, services, and actions. Every process is a node; nodes exchange data over topics (pub/sub) or services/actions (request/reply). The CLI tools below let you inspect this graph at runtime without reading source code — essential for debugging integration issues between packages.

1

Node Introspection — ros2 node

Inspect running nodes: list all nodes, get detailed info about publishers, subscribers, services, and actions provided by a node.

bash
# List all active nodes
ros2 node list

# Verbose list with namespaces shown
ros2 node list -a     # includes hidden /_ros2cli_* nodes

# Detailed node info: pub/sub/services/actions/parameters
ros2 node info /my_robot/lidar_node
# Output:
#   /my_robot/lidar_node
#     Subscribers:
#       /parameter_events: rcl_interfaces/msg/ParameterEvent
#     Publishers:
#       /my_robot/scan: sensor_msgs/msg/LaserScan
#       /rosout: rcl_interfaces/msg/Log
#     Service Servers:
#       /my_robot/lidar_node/describe_parameters
#       /my_robot/lidar_node/get_parameters
#       ...
#     Action Servers:
#       (none)
#     Action Clients:
#       (none)

# Show all nodes with their types (pub/sub/svc counts)
ros2 node list --count-subscribers --count-publishers 2>/dev/null || ros2 node list
2

Topic Introspection — ros2 topic

Inspect topics: list, echo messages, get interface type, check publish rate (Hz), bandwidth, and inject test messages from the CLI.

bash
# List all active topics
ros2 topic list

# List with message type shown
ros2 topic list -t

# Show topic type
ros2 topic type /scan
# → sensor_msgs/msg/LaserScan

# Echo messages on a topic (Ctrl-C to stop)
ros2 topic echo /scan

# Echo with --no-arr to skip large arrays, --once for a single message
ros2 topic echo /scan --no-arr
ros2 topic echo /scan --once

# Show full message definition (field names + types)
ros2 topic info /scan -v
ros2 interface show sensor_msgs/msg/LaserScan

# Measure publish rate (Hz)
ros2 topic hz /scan
# → average rate: 10.003, min: 0.099s, max: 0.101s

# Measure bandwidth
ros2 topic bw /scan
# → Subscribed to [/scan]: 183.86 KB/s from 1 publisher

# Count subscribers and publishers
ros2 topic info /scan
# → Type: sensor_msgs/msg/LaserScan
# → Publisher count: 1
# → Subscription count: 2

# Publish a test message from CLI (YAML syntax)
ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \
  "{linear: {x: 0.5, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.2}}"

# Publish once (--once) at rate (--rate N)
ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist \
  "{linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}"

ros2 topic pub --rate 10 /my_topic std_msgs/msg/String "data: hello"
3

Service & Action Introspection

Inspect services and actions at runtime: list active servers, show types, call services, and send action goals from the CLI.

bash
# ─── Services ────────────────────────────────────────────────
ros2 service list
ros2 service list -t                   # with types
ros2 service type /my_node/set_params
ros2 interface show rcl_interfaces/srv/SetParameters

# Call a service (YAML request body)
ros2 service call /set_bool std_srvs/srv/SetBool "{data: true}"
# → requester: making request: std_srvs.srv.SetBool_Request(data=True)
# → response: std_srvs.srv.SetBool_Response(success=True, message='')

# ─── Actions ─────────────────────────────────────────────────
ros2 action list
ros2 action list -t                    # with types
ros2 action type /navigate_to_pose
ros2 action info /navigate_to_pose     # shows servers + clients

# Send a goal to an action server
ros2 action send_goal /navigate_to_pose \
  nav2_msgs/action/NavigateToPose \
  "{pose: {header: {frame_id: map}, pose: {position: {x: 1.0, y: 2.0, z: 0.0}, orientation: {w: 1.0}}}}"

# Send goal with feedback stream
ros2 action send_goal -f /navigate_to_pose \
  nav2_msgs/action/NavigateToPose \
  "{pose: {header: {frame_id: map}, pose: {position: {x: 1.0, y: 0.0, z: 0.0}, orientation: {w: 1.0}}}}"
4

rqt_graph — Visual Node Graph

rqt_graph renders the live ROS 2 computation graph: nodes as circles, topics as rectangles, arrows showing pub/sub connections.

bash
# Install rqt_graph (if not already installed)
sudo apt install ros-${ROS_DISTRO}-rqt-graph

# Launch rqt_graph (opens a GUI window)
ros2 run rqt_graph rqt_graph

# Or open it from the rqt umbrella tool
rqt --standalone rqt_graph

# Useful rqt_graph options (set in the GUI toolbar):
#   - "Nodes only" — shows only nodes, hides topics
#   - "Nodes/Topics (active)" — only shows active connections
#   - "Nodes/Topics (all)" — includes latched/inactive
#   - Hide Dead Sinks checkbox — removes nodes with no subscribers
#   - Hide TF checkbox — hides the busy /tf topic
#   - Refresh button — force redraw (graph auto-updates)

# rqt_console: live log viewer with filter by severity/node
ros2 run rqt_console rqt_console

In headless environments (CI, Docker without display), use DISPLAY=:0 or Xvfb. For programmatic graph snapshots, parse ros2 node/topic list output instead.

5

Namespaces and Topic Remapping

Namespaces scope nodes and their topics. Remapping redirects a topic/service/parameter name at launch time without changing source code.

bash
# Run a node in a namespace
ros2 run my_pkg my_node --ros-args --remap __ns:=/robot1
# → node name becomes /robot1/my_node
# → topics become /robot1/scan, /robot1/cmd_vel, etc.

# Remap a specific topic
ros2 run my_pkg lidar_node --ros-args --remap /scan:=/robot1/scan

# Remap multiple things
ros2 run my_pkg my_node --ros-args \
  --remap __ns:=/robot1 \
  --remap __node:=front_lidar \
  --remap /scan:=/robot1/front/scan \
  -p scan_rate:=20.0

# In a launch file (Python):
from launch_ros.actions import Node
Node(
    package="my_pkg",
    executable="my_node",
    namespace="robot1",                    # sets __ns
    name="front_lidar",                    # overrides __node
    remappings=[
        ("/scan", "/robot1/front/scan"),   # topic remap
    ],
    parameters=[{"scan_rate": 20.0}],
)
6

Programmatic Graph Introspection (Python API)

Access the node graph programmatically: get_node_names, get_publisher_names_and_types_by_node, get_topic_names_and_types — useful for monitoring nodes.

python
import rclpy
from rclpy.node import Node


class GraphInspector(Node):
    def __init__(self):
        super().__init__("graph_inspector")

    def inspect(self) -> None:
        # ─── Nodes ────────────────────────────────────────────
        nodes = self.get_node_names()
        self.get_logger().info(f"Active nodes ({len(nodes)}):")
        for n in nodes:
            self.get_logger().info(f"  {n}")

        # ─── Topics with types ────────────────────────────────
        topics = self.get_topic_names_and_types()
        self.get_logger().info(f"Active topics ({len(topics)}):")
        for name, types in topics:
            self.get_logger().info(f"  {name}  [{', '.join(types)}]")

        # ─── Publishers on a specific node ────────────────────
        pubs = self.get_publisher_names_and_types_by_node(
            "lidar_node", "robot1"  # node_name, namespace
        )
        for name, types in pubs:
            self.get_logger().info(f"  pub: {name}  {types}")

        # ─── Subscribers on a specific node ───────────────────
        subs = self.get_subscriber_names_and_types_by_node(
            "lidar_node", "robot1"
        )

        # ─── Services ─────────────────────────────────────────
        services = self.get_service_names_and_types()

        # ─── Actions (via action introspection) ────────────────
        # Actions use topic patterns under the hood:
        # /action_name/_action/feedback_with_type
        # /action_name/_action/status


def main(args=None):
    rclpy.init(args=args)
    node = GraphInspector()
    node.inspect()
    node.destroy_node()
    rclpy.shutdown()


if __name__ == "__main__":
    main()

get_node_names_and_namespaces() returns (name, namespace) tuples. get_topic_names_and_types() returns all discovered topics, not just those with active subscriptions.

7

ros2 doctor — System Health Check

ros2 doctor checks your ROS 2 environment for common configuration issues: DDS settings, clock sync, network, environment variables.

bash
# Full environment health report
ros2 doctor

# Verbose report (shows all checks, even passing ones)
ros2 doctor --report

# Check specific subsystem
ros2 doctor --report | grep -A5 "network"

# Show which DDS implementation is active
ros2 doctor --report | grep -i dds

# Common issues ros2 doctor catches:
#   - RMW_IMPLEMENTATION not set (using default cyclone/fastrtps)
#   - ROS_DOMAIN_ID mismatch between processes
#   - PYTHONPATH conflicts
#   - Missing DDS discovery peers
#   - System clock not synced (NTP issues)

# Check environment variables
echo $ROS_DOMAIN_ID          # default 0 — processes in same domain discover each other
echo $RMW_IMPLEMENTATION     # e.g., rmw_cyclonedds_cpp, rmw_fastrtps_cpp
echo $ROS_LOCALHOST_ONLY     # 1 = only discover nodes on localhost
8

tf2 Frame Graph Debugging

The tf2 system broadcasts coordinate frame transforms. Use CLI tools to view the frame tree, check transform availability, and debug TF issues.

bash
# View the complete TF tree (requires running TF publishers)
ros2 run tf2_tools view_frames
# → writes frames.pdf to current directory

# Echo a specific transform
ros2 run tf2_ros tf2_echo map base_link
# → At time 1719000000.0
# →   Translation: (1.23, 0.45, 0.00)
# →   Rotation: (0.000, 0.000, 0.123, 0.992)

# Check if a transform is available within a timeout
ros2 run tf2_ros tf2_echo map base_link 3.0    # 3s timeout

# Monitor TF topic directly
ros2 topic echo /tf
ros2 topic echo /tf_static

# TF buffer lookup in Python
from tf2_ros import Buffer, TransformListener
import rclpy

rclpy.init()
node = rclpy.create_node("tf_checker")
tf_buffer = Buffer()
tf_listener = TransformListener(tf_buffer, node)

# Wait then look up transform
try:
    t = tf_buffer.lookup_transform("map", "base_link",
                                   rclpy.time.Time())
    print(f"x={t.transform.translation.x:.3f}")
except Exception as e:
    print(f"TF error: {e}")

CLI Cheatsheet

CommandDescription
ros2 node listList all active nodes
ros2 node info /node_nameShow pubs/subs/services/actions of a node
ros2 topic list -tList topics with message types
ros2 topic echo /topic --oncePrint one message from a topic
ros2 topic hz /topicMeasure publish rate
ros2 topic bw /topicMeasure topic bandwidth
ros2 topic pub /topic Type '{...}'Inject a message from CLI
ros2 interface show pkg/msg/TypeShow message/service/action definition
ros2 service call /svc Type '{...}'Call a service from CLI
ros2 action send_goal /act Type '{...}'Send an action goal from CLI
ros2 doctor --reportFull environment health check
ros2 run tf2_tools view_framesGenerate TF frame tree PDF