Skip to main content
🗺️ Nav2 CostmapROS 2 · June 2026

ROS 2 Nav2 Costmap2D Guide 2026

Configure Nav2 costmaps for reliable robot navigation: global costmap with StaticLayer + ObstacleLayer, local rolling-window costmap with VoxelLayer for 3D sensors, and InflationLayer tuning with inflation_radius and cost_scaling_factor.

Two Costmaps, One Robot

Nav2 uses two costmaps simultaneously: the global costmap (full map, updated slowly) for path planning from A to B, and the local costmap (3-5m rolling window, updated fast) for obstacle avoidance as the robot moves. Each is configured independently in nav2_params.yaml.

1

Nav2 Costmap2D — Layered Architecture

Nav2's costmap2d maintains two costmaps: global (for path planning) and local (for real-time obstacle avoidance). Each costmap is built from stacked plugins called layers.

text
Nav2 Costmap2D Architecture
═══════════════════════════════════════════════════════

Global Costmap (used by planner_server)        Local Costmap (used by controller_server)
  Dimensions: typically 5–50 m                  Rolling window: 3–5 m radius around robot
  Resolution: 0.05 m/cell (5 cm)                Resolution: 0.05 m/cell
  Update rate: 1–5 Hz                           Update rate: 10–20 Hz

Layer stack (applied in order, bottom to top):

┌──────────────────────────────────────┐
│  InflationLayer                      │  Expands obstacles by robot radius
│  (inflation_radius: 0.55 m)          │  Creates gradient cost → planner stays away
├──────────────────────────────────────┤
│  ObstacleLayer                       │  Marks/clears from LiDAR scan
│  (sensor: /scan or /pointcloud)      │  Raycasting clears free space
├──────────────────────────────────────┤
│  StaticLayer (global only)           │  Map server occupancy grid
│  (/map topic, costmap.yaml)          │  Known walls and static obstacles
└──────────────────────────────────────┘

Cost values (0–254):
  0    = FREE (safe to drive)
  1–252 = Cost gradient (inflation zone)
  253  = INSCRIBED (robot center would touch obstacle)
  254  = LETHAL (occupied — obstacle itself)
  255  = NO_INFORMATION (unknown)

Key parameters:
  robot_radius  — used by inflation layer
  inflation_radius  — how far to inflate obstacles
  cost_scaling_factor  — inflation gradient steepness
2

Global Costmap YAML Configuration

The global costmap uses StaticLayer + ObstacleLayer + InflationLayer. It covers the full map and is updated at low frequency.

yaml
# nav2_params.yaml — global_costmap section
global_costmap:
  global_costmap:
    ros__parameters:
      # Coordinate frame
      global_frame: map
      robot_base_frame: base_link
      
      # Update rates
      update_frequency: 1.0      # Hz — how often to update the costmap
      publish_frequency: 1.0     # Hz — how often to publish for RViz
      
      # Map dimensions (auto from map_server if using StaticLayer)
      resolution: 0.05           # meters per cell
      
      # Robot footprint (circle OR polygon)
      robot_radius: 0.22         # for circular robots (use footprint: for polygon)
      # footprint: "[[0.25, 0.22], [0.25, -0.22], [-0.25, -0.22], [-0.25, 0.22]]"
      
      # Layer plugins (applied in order listed)
      plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
      
      static_layer:
        plugin: "nav2_costmap_2d::StaticLayer"
        map_subscribe_transient_local: True   # latched /map subscription
        enabled: True
      
      obstacle_layer:
        plugin: "nav2_costmap_2d::ObstacleLayer"
        enabled: True
        observation_sources: scan    # comma-separated list of sensors
        scan:
          topic: /scan               # LaserScan topic
          max_obstacle_height: 2.0   # m — ignore points above this
          clearing: True             # raycasting clears free space
          marking: True              # sensor marks obstacles
          data_type: "LaserScan"
          raytrace_max_range: 3.0    # m — max raytrace distance
          raytrace_min_range: 0.0
          obstacle_max_range: 2.5    # m — max obstacle marking range
          obstacle_min_range: 0.0
      
      inflation_layer:
        plugin: "nav2_costmap_2d::InflationLayer"
        cost_scaling_factor: 3.0    # larger = steeper gradient (faster decay)
        inflation_radius: 0.55      # m — match or exceed robot_radius

inflation_radius should be >= robot_radius. A value 10-20% larger gives the planner safety margin. cost_scaling_factor=3.0 creates a smooth gradient; lower values (1.0-2.0) make the robot stay further from walls.

3

Local Costmap YAML — Rolling Window

The local costmap uses a rolling window centered on the robot. It only contains ObstacleLayer + InflationLayer (no StaticLayer — the robot shouldn't plan around the global map at close range).

yaml
# nav2_params.yaml — local_costmap section
local_costmap:
  local_costmap:
    ros__parameters:
      # Coordinate frame — odom (not map) for local real-time updates
      global_frame: odom
      robot_base_frame: base_link
      
      # Rolling window settings
      rolling_window: true
      width: 3                   # meters wide (centered on robot)
      height: 3                  # meters tall
      resolution: 0.05           # 5 cm cells
      
      # Update at higher frequency than global costmap
      update_frequency: 5.0      # Hz
      publish_frequency: 2.0     # Hz
      
      robot_radius: 0.22
      
      # Local costmap: no StaticLayer (would be too slow to update)
      plugins: ["obstacle_layer", "inflation_layer"]
      
      obstacle_layer:
        plugin: "nav2_costmap_2d::ObstacleLayer"
        enabled: True
        observation_sources: scan
        scan:
          topic: /scan
          max_obstacle_height: 2.0
          clearing: True
          marking: True
          data_type: "LaserScan"
          raytrace_max_range: 3.0
          obstacle_max_range: 2.5
      
      inflation_layer:
        plugin: "nav2_costmap_2d::InflationLayer"
        cost_scaling_factor: 3.0
        inflation_radius: 0.55
      
      # Keep obstacles for 0.5 seconds even after sensor stops seeing them
      # Helps with dynamic obstacles that briefly leave sensor FOV
      # obstacle_layer.track_unknown_space: false  # treat unknown as free

Use global_frame: odom (not map) for the local costmap. The local costmap doesn't need map correction — it only needs to track obstacles relative to the robot's immediate environment.

4

3D Obstacle Layer — PointCloud2 Sensors

For 3D LiDAR or RGBD cameras, use VoxelLayer (3D voxel grid) instead of ObstacleLayer (2D scan). VoxelLayer handles multi-height obstacles correctly.

yaml
# 3D sensors: use VoxelLayer instead of ObstacleLayer
global_costmap:
  global_costmap:
    ros__parameters:
      plugins: ["static_layer", "voxel_layer", "inflation_layer"]
      
      voxel_layer:
        plugin: "nav2_costmap_2d::VoxelLayer"
        enabled: True
        publish_voxel_map: True           # debug visualization
        origin_z: 0.0
        z_resolution: 0.05               # vertical voxel height (5 cm)
        z_voxels: 16                      # 16 voxels × 5 cm = 0.8 m height
        max_obstacle_height: 2.0
        mark_threshold: 0                 # voxels needed to mark a cell
        observation_sources: pointcloud
        
        pointcloud:
          topic: /cloud/filtered          # processed PointCloud2
          max_obstacle_height: 2.0
          min_obstacle_height: 0.05       # ignore floor returns
          clearing: True
          marking: True
          data_type: "PointCloud2"
          raytrace_max_range: 3.0
          obstacle_max_range: 2.5

# ── For RGBD cameras (RealSense, Orbbec) ────────────────────
# Use depth_image_proc to convert to PointCloud2 first:
# ros2 run depth_image_proc point_cloud_xyz_node \
#   --ros-args \
#   -r image_rect:=/camera/depth/image_rect_raw \
#   -r camera_info:=/camera/depth/camera_info

# ── Footprint instead of robot_radius ────────────────────────
# For non-circular robots (rectangular, etc.)
global_costmap:
  global_costmap:
    ros__parameters:
      footprint: "[[0.25, 0.22], [0.25, -0.22], [-0.25, -0.22], [-0.25, 0.22]]"
      footprint_padding: 0.01    # m — extra safety padding
5

Debug Costmap Problems

When the robot won't navigate, can't find a path, or drives too close to walls, use these techniques to inspect costmap state.

bash
# ── Visualize costmaps in RViz2 ─────────────────────────────
# Add → By Topic:
#   /global_costmap/costmap           — global occupancy grid
#   /global_costmap/costmap_updates   — efficient updates
#   /local_costmap/costmap            — local rolling window
# Display options:
#   Color scheme: costmap (shows inflation gradient in blue→red)
#   Max color scale value: 100

# ── CLI inspection ────────────────────────────────────────────
# Check costmap update rate
ros2 topic hz /global_costmap/costmap_updates
ros2 topic hz /local_costmap/costmap_updates

# Inspect a single costmap message
ros2 topic echo /global_costmap/costmap --once

# ── Common problems and fixes ─────────────────────────────────
# Problem: "No plan found" even in open space
# Cause:   inflation_radius too large → whole corridor is INSCRIBED
# Fix:     Reduce inflation_radius or footprint size

# Problem: Robot drives through obstacles
# Cause:   ObstacleLayer sensor not publishing or wrong QoS
# Fix:     ros2 topic hz /scan  →  if 0 Hz, fix sensor driver
#          Check data_type matches (LaserScan vs PointCloud2)

# Problem: Ghost obstacles (obstacles stuck in costmap after clearing)
# Cause:   clearing: False in observation_sources
# Fix:     Set clearing: True; raytrace_max_range >= max sensor range

# Problem: Costmap never updates
# Cause:   TF not available (global_frame → robot_base_frame)
# Fix:     ros2 run tf2_ros tf2_echo map base_link
#          → if fails, TF tree is broken (fix odom→base_link)

# ── Dynamic reconfigure costmap params at runtime ─────────────
ros2 param set /global_costmap/global_costmap inflation_layer.inflation_radius 0.3
ros2 param set /local_costmap/local_costmap obstacle_layer.enabled false

Quick Reference

ParameterDetails
global_frame (global)map — aligned to map server
global_frame (local)odom — follows robot, no map correction needed
rolling_windowtrue for local costmap — centers on robot
StaticLayerLoads /map occupancy grid — global costmap only
ObstacleLayerLaserScan marking/raycasting — 2D sensors
VoxelLayerPointCloud2 3D voxels — use for RGBD/3D LiDAR
InflationLayerExpands obstacles by inflation_radius
robot_radiusMust match physical robot — used by InflationLayer
inflation_radius≥ robot_radius; larger = more conservative paths
cost_scaling_factor3.0 = steep gradient; 1.0 = shallow (stays farther from walls)