Godot pathfinding / 2026-08-24 / 16 min read
Verified as of Godot 4.7.2 stable, artifact verified 2026-08-24
NavigationServer3D in Godot 4: Path Queries, Map Sync, and Debugging
Use NavigationServer3D directly in Godot 4. Query a map RID, wait for synchronization, diagnose short paths, and inspect query metadata.

The direct answer
The first path you ask NavigationServer3D for can come back empty because the server has not published the map yet. After that is fixed, a path with several points can still stop at the edge of a platform. Both outcomes follow the API contract, but each needs a different check.
This is the 3D counterpart to the NavigationServer2D guide. If you are starting with an empty scene, the 3D pathfinding starter owns setup, baking, click-to-move, and body movement. This page starts with a map RID and a direct query.
NavigationServer3D answers from the last map iteration it published. Setters enter a physics-frame synchronization phase, and asynchronous region or map work can take additional frames. A map you just changed may not be the map you are querying yet. map_get_iteration_id(map) is 0 until the first publication and changes when the map publishes another iteration.
A non-empty map_get_path() result means the server found a route from the snapped start toward the closest reachable navigation-mesh point near the target. The final point is the evidence. Compare it with a snapped target reference instead of the raw request, which may float above or beside the mesh.
Two defaults matter before you trust a short path. map_get_path() creates a default NavigationPathQueryParameters3D, which caps the search at path_search_max_polygons = 4096. Its path_search_max_distance = 0.0 leaves distance unlimited. On a large or fragmented mesh, the polygon cap can also end a route early. For diagnosis, use a query object and set both limits to 0.
Start here: symptom to cause to fix
Reproduce the symptom, then jump to the check that gives you a number. Run the checked query before changing any NavigationAgent3D setting. Path-data failures and movement failures look identical from outside.
Observable tell: each route below ends in an iteration, endpoint error, metadata owner, or known query result that can confirm or reject the suspected cause.
| What you see | Most likely cause | Go here |
|---|---|---|
Empty path during startup and map_get_iteration_id() == 0 | The map has not published a usable iteration. The path query itself may be silent. Closest-point getters emit the debug warning | Check synchronization |
| Path has points but appears to end before the raw target | The target is off-mesh, disconnected, behind an excluded region, or the search hit its cap | Run the checked query |
| The route is stale after a runtime rebake | The bake finished but the region or map has not published a newer usable iteration | Check rebake readiness |
| You need the region or link behind each path point | Metadata is disabled, or the short map_get_path() API is hiding it | Use query objects |
| Two regions touch but the path stops at the seam | Their edges do not satisfy the direct endpoint-match case or the proximity margin | Check the 3D caveats |
| The path uses a link but the actor stops at its start | Pathfinding found the link. Gameplay code did not perform the traversal | Handle the link handoff |
| A worker-thread request stalls or spikes | More callers are active than the query-slot ceiling, or SceneTree parsing left the main thread | Separate queries from parsing |
When you need the server directly
NavigationAgent3D is a wrapper. It holds an RID, forwards property changes to the server, and advances its path logic when you call get_next_path_position() each physics frame. It issues new queries when its state requires them. That is the right tool for moving one body toward a point. It is the wrong tool when the question is about the map itself.
Use the server when you need a query without owning an agent, create maps or regions procedurally, inspect the region and link behind each path point, let one caller reuse a query-object pair, or isolate a map failure from a movement failure. Direct queries give you a verdict with numbers attached.
Everything on the server is identified by an RID. Maps are isolated navigation worlds. Regions contribute NavigationMesh polygons to one map at a time. Links add deliberate edges. Agents and obstacles participate in avoidance. Release every RID you create with NavigationServer3D.free_rid().
Paste this first: a checked path query
Drop this into a Node3D in the world you are querying and call it with global start and target positions. It prints one verdict and returns the same evidence as a dictionary.
The raw target may sit above the floor or slightly outside the mesh and still be reachable. After a synchronized map returns a path, the helper obtains a target reference with map_get_closest_point(). It reports that gap as target_snap_error, then compares the final path point with the snapped reference as endpoint_error. reached means the endpoint is inside tolerance. short means it is not.
map_get_closest_point() ignores navigation layers. The helper safely inspects the closest owner and reports whether that target owner matches the query mask. That is supporting evidence, not a complete diagnosis. An allowed target can still be unreachable because an intermediate region is excluded, as the artifact's layer-2 bridge demonstrates.
The default tolerance of 0.5 is two default cells. It is an editorial starting point, not an engine constant. Pick a tolerance from the game scale.
Observable tell: the returned dictionary separates iteration state, path-array state, target snap, final endpoint error, and the map-wide target owner's layer evidence.
Maps, RIDs, and synchronization
Most NavigationServer3D changes are queued instead of becoming visible immediately. The server accepts setter calls and processes them in a physics-frame synchronization phase. With asynchronous map and region iterations enabled, publishing the resulting navigation data can take additional frames.
Queries read the last published state. In the verified Godot 4.7.2 fixture, map_get_path() at iteration 0 returned an empty path without an engine error. The source's one-time warning about a query before first synchronization belongs to closest-point getters. Read the iteration ID instead of depending on console text.
The minimum guard below clears only the iteration-zero state. The checked query that follows is the semantic postcondition and may still report empty or short. For deterministic startup, also watch the relevant region iterations and poll a known query until it gives the expected answer or times out.
The final artifact run reached a usable known route at map iteration 2, after four observed physics frames and two map-specific map_changed emissions. These are observations from one controlled run, not engine timing guarantees.
Iteration IDs are unsigned 32-bit values that wrap to 1. Compare them with !=, not >. Maps and regions have separate IDs because their asynchronous update stages are separate.
Do not use map_force_update() as a readiness barrier. Godot deprecated it in 4.5 as incompatible with asynchronous updates and safe only in a single-threaded context. Poll the relevant iteration, or observe map_changed, then prove readiness with a query.
Observable tell: a non-zero map iteration clears the pre-sync state. The expected region iterations and a known query result establish usable geometry.
Rebake readiness: region, then map, then a query
A runtime rebake has three finish lines. Bake completion alone does not prove that the region and map have published the new navigation data. Next the region publishes an iteration, then the map rebuilds and publishes its own.
Capture both baselines after bake_finished, wait for the region iteration to change, then the map iteration, apply a timeout, and finish with a validation query that has a known answer in the game. A counter changing is a precondition, not the final proof.
The artifact verifies the same region-then-map-then-query sequence with a bridge enable or disable mutation. It does not execute runtime baking. This helper is parser-gated and backed by the 4.7 API and source contract rather than by the artifact's runtime gate.
Observable tell: the region and map IDs both differ from their baselines, then the known query returns the expected post-mutation status.
Query objects and metadata
map_get_path() is the short API. Use NavigationPathQueryParameters3D and NavigationPathQueryResult3D when you need the region or link behind each point, total path length, post-processing, or search limits you control.
A serial caller can create one parameter and result pair and reuse it. Do not mutate and share that pair across concurrent in-flight queries. Give each worker or caller its own objects. The query functions are thread-safe, but one shared mutable request is not the concurrency model tested here.
The result exposes path, scalar path_length, and the metadata arrays path_types, path_rids, and path_owner_ids. Those three arrays align with path points by index. The artifact checks the bridge RID, owner, and region type together at index 1, and the right-region association together at final index 3.
For diagnostics, set both search limits to 0. A capped search can build a route to the polygon found closest to the target so far, which resembles a disconnected result until the active limit is known.
Observable tell: each suspect path point has a same-index segment type, RID, and owner ID, while path_length matches the sum of the returned segments.
| Option | Use it when | Trade-off |
|---|---|---|
PATH_POSTPROCESSING_CORRIDORFUNNEL | Actors move freely inside irregular polygons | Shortest movement path shaped by the corridor |
PATH_POSTPROCESSING_EDGECENTERED | Equal-sized polygons or grid-like movement | Longer path with predictable edge waypoints |
PATH_POSTPROCESSING_NONE | You need the raw selected corridor | Diagnostic output rather than a movement path |
simplify_path and simplify_epsilon | Minor points create steering jitter | Additional query-time work |
included_regions and excluded_regions | The map is partitioned into known chunks | You maintain the RID lists. Exclusion wins when a region is in both |
path_return_max_length and path_return_max_radius | You want a clipped route leg | The path ends early by design and is not an unreachable verdict |
path_search_max_polygons and path_search_max_distance | Production queries need a ceiling | Use zero for diagnosis. A low limit returns a poor short path |
Short 3D caveats and where they go next
Cell size and cell height must match the map. The map and NavigationMesh defaults are both 0.25. A debug build warns when the map cell is larger than the mesh cell because mismatched settings can damage edge rasterization. Baking, obstacle carving, and separate maps for different actor sizes deserve their own treatment.
Seams are proven by a query, not a counter. The documented direct-merge case requires matching edge endpoint positions. Otherwise, nearly parallel edges can connect through edge_connection_margin. A direct edge-key merge does not appear in the proximity-connection count, and the free-edge counter also includes ordinary outer boundaries. The proof of a seam is a checked route across it.
A link does not move anyone. When an agent reaches a link position, gameplay code must perform the jump, teleport, ladder, door, or platform traversal. With metadata enabled, NavigationAgent3D.link_reached exposes the owner and entry or exit positions.
Queries are thread-safe, with a ceiling. Godot's 4.7 thread-safe API guide allows navigation queries from threads and true parallel execution. Additional callers wait when the navigation/pathfinding/max_threads slot pool is full. The default is 4. SceneTree source-geometry parsing remains a main-thread operation.
Avoidance is not pathfinding. An avoidance obstacle changes an agent's safe velocity. Avoidance alone does not change the route. A region, link, layer, or navigation-mesh data update can change it. Continue with NavigationObstacle avoidance vs pathfinding when local steering is the actual symptom.
Proof scope and measurement-gap. The downloadable receipt deliberately does not prove runtime baking, proximity seams, links, avoidance, concurrent throughput, timings, or arbitrary imported navigation geometry. It proves the seven controlled server-level behaviors named in the receipt.
Implementation checklist
Use this order for a new scene and for a path that is empty or ends short.
Observable tell: the same known query returns the expected status after synchronization. Any remaining failure now belongs to agent following, body movement, or special traversal rather than the direct route data.
- Bake a
NavigationMeshon aNavigationRegion3Dwhosecell_sizeandcell_heightmatch the map. - Defer setup and poll with a timeout until
map_get_iteration_id(map) != 0. Treat that as an iteration-zero guard, not proof that every expected region is current. - Snap the target with
map_get_closest_point()and recordtarget_snap_error. Remember that the snap is map-wide rather than layer-filtered. - Query with
map_get_path()and compare the final point with the snapped target. Require the known expected status as the final startup postcondition. - If the route is short, repeat it with a query object, zero search limits, and
INCLUDE_ALLmetadata. Read the owner of the final point. - After a region mutation, capture baselines, wait for the region and map iterations to change, then repeat a query whose answer is known. For map-only changes, wait on the relevant map postcondition.
- Only then debug
NavigationAgent3Dand the body.
Frequently asked questions
Why does NavigationServer3D return an empty path on the first frame?
The map may not have published a usable iteration yet. Server setters enter a physics-frame synchronization phase, while asynchronous region and map work can take more frames. Defer setup, poll map_get_iteration_id(map) with a timeout, and confirm readiness with a query whose result you know.
How do I know if a NavigationServer3D path reached the target?
Snap the target with map_get_closest_point(), then compare the final path point with that snapped reference. The raw target can float above or beside the mesh and still be reachable. The snap is map-wide, so on layer-filtered queries inspect its owner as supporting evidence rather than proof of where the route failed.
Why does map_get_path() stop early on a large navmesh?
map_get_path() uses a default query object whose path_search_max_polygons is 4096. When the cap is reached, the search can return a route toward the closest polygon found so far. For diagnosis, use query_path() with polygon and distance limits set to 0.
Is NavigationServer3D.map_get_path() thread-safe?
Yes. Godot 4.7 documents navigation queries as thread-safe and able to run in true parallel, with additional callers waiting at the max_threads slot ceiling. Do not share one mutable parameter or result pair across in-flight queries. SceneTree parsing for baking remains main-thread only.
Should I call map_force_update() to query in the same frame?
No. Godot deprecated it in 4.5 because it is incompatible with asynchronous updates and safe only in a single-threaded context. Poll map_get_iteration_id() or observe map_changed, then confirm the expected result with a query.
Does map_changed mean the map is ready after a rebake?
Not by itself. Region and map updates are separate asynchronous stages, and an emission can belong to another mutation. Capture the region and map iteration IDs after bake_finished, wait for both to change, then run a query whose answer you know.