Quick answer: Orphaned nodes are created when you call remove_child() on a node but never call queue_free() on it, or when you instantiate a node with .new() or instantiate() but never add it to the scene tree.
Learning how to find memory leaks in Godot games is a common challenge for game developers. Your Godot game runs fine for the first ten minutes, but after an hour the frame rate drops, the system memory usage has doubled, and eventually the game crashes. You have a memory leak. Unlike languages with automatic garbage collection that handles everything, Godot's Node system requires you to explicitly free objects when you are done with them. A single forgotten queue_free in a spawning loop can leak thousands of nodes per play session. This guide shows you how to find exactly where memory is leaking and how to fix it.
How Memory Management Works in Godot
Godot has two memory management systems running simultaneously. RefCounted objects (Resources, some utility classes) use reference counting and are automatically freed when no references point to them. Node and Object instances use manual memory management — you must explicitly call free or queue_free to release them.
When you call queue_free on a node, it is removed from the scene tree and freed at the end of the current frame. When you call remove_child, the node is detached from the tree but not freed. It continues to exist in memory as an orphaned node. This distinction is the root cause of most memory leaks in Godot games.
# This LEAKS memory: node is removed but never freed
func despawn_enemy(enemy: Node2D) -> void:
remove_child(enemy) # Detached from tree, still in memory
# This is CORRECT: node is freed properly
func despawn_enemy(enemy: Node2D) -> void:
enemy.queue_free() # Freed at end of frame
# This is also CORRECT: remove then free later (object pooling)
var pool: Array[Node2D] = []
func despawn_enemy(enemy: Node2D) -> void:
remove_child(enemy)
pool.append(enemy) # Tracked for reuse or later cleanup
Detecting Leaks with Performance Monitors
Godot's Debugger panel includes a Monitors tab that tracks engine-level metrics in real time. Three monitors are essential for detecting memory leaks:
Object
Orphan Nodes # Nodes that exist but are not in the scene tree
Object Count # Total objects currently alive in memory
Node Count # Nodes currently in the scene tree
Memory
Static Memory # Total memory allocated by the engine
Enable these monitors and play your game. Transition between scenes several times. Spawn and destroy enemies, bullets, particles, and UI elements. If the Orphan Nodes count climbs and never comes back down, you are leaking nodes. If Object Count increases without stabilizing, something is being created and never released.
A healthy game shows the Orphan Nodes count at zero or near zero during steady gameplay. Brief spikes during scene transitions are normal (nodes being freed asynchronously), but the count should return to baseline within a frame or two.
Using print_orphan_nodes()
Once you know orphaned nodes exist, you need to find out what they are. Godot provides a built-in function for this. Call it from the debugger console or from code:
# Call from a debug key binding to dump orphan info
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("debug_orphans"):
print_orphan_nodes()
# Output in the console looks like:
# ORPHAN NODE: Type: CharacterBody2D, Name: Enemy_42
# ORPHAN NODE: Type: Sprite2D, Name: BulletSprite_391
# ORPHAN NODE: Type: GPUParticles2D, Name: ExplosionFX
# ORPHAN NODE: Type: Label, Name: DamageNumber_88
This output tells you the type and name of every orphaned node. In this example, enemies, bullet sprites, explosion particles, and floating damage numbers are all being leaked. Each of these is being created during gameplay but never properly freed. The names often contain clues about where in your code the node was instantiated.
Programmatic Leak Detection
For automated testing, track the orphan count programmatically and assert that it stays within acceptable bounds. This catches regressions early:
# Memory leak detection utility
class_name MemoryLeakDetector
extends Node
var baseline_orphans: int = 0
var baseline_objects: int = 0
var check_interval: float = 5.0
func _ready() -> void:
# Record baseline after scene is fully loaded
await get_tree().process_frame
baseline_orphans = Performance.get_monitor(
Performance.OBJECT_ORPHAN_NODE_COUNT)
baseline_objects = Performance.get_monitor(
Performance.OBJECT_COUNT)
var timer := Timer.new()
timer.wait_time = check_interval
timer.timeout.connect(_check_for_leaks)
add_child(timer)
timer.start()
func _check_for_leaks() -> void:
var current_orphans: int = Performance.get_monitor(
Performance.OBJECT_ORPHAN_NODE_COUNT)
var current_objects: int = Performance.get_monitor(
Performance.OBJECT_COUNT)
var orphan_delta: int = current_orphans - baseline_orphans
if orphan_delta > 50:
push_warning(
"Memory leak detected: %d orphaned nodes above baseline"
% orphan_delta)
print_orphan_nodes()
Add this as an autoload in development builds. It will warn you in the console whenever orphan nodes accumulate beyond a threshold, printing their types so you can immediately identify the source.
Signal Connections and Reference Leaks
Signals are a common source of subtle memory issues. When object A connects a signal to a method on object B, the signal system holds a reference between them. This can prevent cleanup in two ways:
Stale connections to freed objects. If object B is freed while object A still has a signal connected to it, emitting that signal causes an error. Godot 4 handles this more gracefully than Godot 3, but it is still a source of bugs and unexpected behavior.
Connections preventing logical cleanup. If you hold a reference to a node in a variable and that node is supposed to be freed during a scene transition, the reference keeps the node from being treated as garbage by your own logic even though Godot itself does not use reference counting for Nodes. The real problem is when you try to access the freed node later.
# PROBLEM: Signal connected across scene boundaries
func _ready() -> void:
GameEvents.enemy_spawned.connect(_on_enemy_spawned)
# If this node is freed but GameEvents is an autoload,
# the connection persists as a stale reference
# SOLUTION: Disconnect in _exit_tree
func _ready() -> void:
GameEvents.enemy_spawned.connect(_on_enemy_spawned)
func _exit_tree() -> void:
GameEvents.enemy_spawned.disconnect(_on_enemy_spawned)
# ALTERNATIVE: Use CONNECT_ONE_SHOT for temporary connections
func _ready() -> void:
player.health_changed.connect(
_on_health_changed, CONNECT_ONE_SHOT)
A good rule of thumb: if you connect a signal to an autoload or a node that outlives the current scene, always disconnect it in _exit_tree. Signals within the same subtree are cleaned up automatically when the parent is freed.
Resource Cache Leaks
Resources loaded with load or preload are cached by default. Loading the same resource path twice returns the same object. This is efficient but can cause unexpected memory retention. A large texture loaded for one scene stays in the cache even after that scene is freed.
# Check what resources are cached
func _debug_resource_cache() -> void:
var cached: PackedStringArray = ResourceLoader.get_cached_ref()
print("Cached resources: ", cached.size())
for path: String in cached:
print(" ", path)
# Force-release a cached resource (use sparingly)
# The resource must have no remaining references
# Setting the local variable to null drops your reference
var big_texture: Texture2D = load("res://huge_background.png")
# ... use it ...
big_texture = null # Drop our reference
# If no other object references it, the cache will release it
For games with many large assets, consider using ResourceLoader.load with the cache_mode parameter set to CACHE_MODE_IGNORE for resources you know are temporary. This prevents them from polluting the cache. Be careful, though — uncached resources are reloaded from disk every time, which is slow.
Scene Transition Leaks
Scene transitions are the most common place where leaks occur. When you call get_tree().change_scene_to_packed(), the old scene is freed and the new one is loaded. But if any code outside the old scene holds a reference to a node inside it — an autoload, a signal connection, a variable in a global script — that reference becomes stale.
# LEAK PATTERN: Global reference to scene-local node
# In autoload GameState.gd:
var current_player: CharacterBody2D
# In Player.gd:
func _ready() -> void:
GameState.current_player = self
# When the scene changes, GameState still holds a reference
# to the old player. The node was freed by the scene change,
# so this is now an invalid reference.
# FIX: Clear the reference when the node exits the tree
func _exit_tree() -> void:
if GameState.current_player == self:
GameState.current_player = null
Use weakref for optional references to nodes that might be freed independently. A weak reference does not prevent the object from being freed and returns null when you try to access a freed object:
# Using weakref for safe optional references
var target_ref: WeakRef
func set_target(node: Node2D) -> void:
target_ref = weakref(node)
func _process(delta: float) -> void:
var target: Node2D = target_ref.get_ref()
if target != null:
look_at(target.global_position)
else:
# Target was freed, handle gracefully
target_ref = null
A Systematic Approach
When you suspect a memory leak, follow this sequence: First, open the Monitors tab and record the orphan node count and total object count. Second, play through the game and perform the actions that you suspect are leaking (spawning, scene changes, UI opens). Third, compare the counts. If they are growing, call print_orphan_nodes to identify the types. Fourth, search your codebase for where those types are instantiated and trace the code path to find the missing queue_free or stale reference.
Related Resources
For general Godot performance tips, see Godot performance profiling guide for beginners. For cross-engine memory debugging, read common causes of game crashes on low-end hardware. For tracking memory issues in bug reports, explore bug reporting tools for Godot developers.
Add the orphan node monitor to your debug HUD and watch it during every playtest. A climbing number is your earliest warning that something is leaking.