Godot 4 'signal already connected' — why it happens and how to fix it
Your game is running, something triggers, and Godot prints a wall of red to the Output panel:
E 0:00:01.887 _ready(): Signal 'area_entered' is already connected to a callable 'Enemy._on_hit'.
The signal usually still fires — but now every connected callback runs twice, so you get double damage, duplicate score increments, and bugs that only appear after a few game cycles. Here are the four causes, ordered by how often they bite.
1. connect() is called in _ready() and the node is freed and reinstantiated
Symptom. The first run works fine. After the player dies and the scene reloads, the error appears on the second play-through.
Cause. When a node is freed and a new instance enters the tree, _ready() runs again.
If you connected a signal to a long-lived object — an autoload, the HUD, a game manager
— that object still holds the connection from the previous instance. A second connect()
stacks a second listener on top.
Fix. Guard with is_connected() or disconnect explicitly in _exit_tree():
func _ready() -> void:
if not hitbox.area_entered.is_connected(_on_hit):
hitbox.area_entered.connect(_on_hit)
func _exit_tree() -> void:
if hitbox.area_entered.is_connected(_on_hit):
hitbox.area_entered.disconnect(_on_hit)
If the connection should only fire once (a pickup, a trigger zone), use the
CONNECT_ONE_SHOT flag instead — Godot disconnects it automatically after one fire:
zone.body_entered.connect(_on_player_enter, CONNECT_ONE_SHOT)
2. connect() is called inside a loop or a function that runs every frame
Symptom. The error prints once per frame (or once per loop iteration), and the callback runs n times more than expected.
Cause. Putting connect() inside _process(), _physics_process(), or a for loop
that iterates over live nodes calls it on every tick or every element. Godot allows
duplicate connections by default; each call stacks another listener.
Fix. Move connect() to _ready() or _enter_tree() — run it once, not repeatedly:
# WRONG — connects every physics frame:
func _physics_process(_delta: float) -> void:
for area in get_overlapping_areas():
area.damaged.connect(_take_damage)
# RIGHT — connects once when the node is ready:
func _ready() -> void:
for child in get_children():
if child.has_signal("damaged"):
child.damaged.connect(_take_damage)
3. Multiple instances all connect to the same shared target
Symptom. One enemy works correctly. Spawn three enemies and every hit triggers the callback three times — or three times on each of the three enemies.
Cause. Each instance calls connect() on the same shared node (an autoload or a
global HUD). Three instances × one connect() each = three listeners. When the signal
fires, all three run.
Fix. Connect signals to self (the instance that owns them), not to a shared target:
# each Enemy connects its own hitbox to its own method — no collision:
func _ready() -> void:
hitbox.area_entered.connect(_on_hit)
func _on_hit(area: Area2D) -> void:
take_damage(area.damage)
If you need a central listener, let the manager register instances and call their methods directly instead of stacking signal connections:
# GameManager.gd
var enemies: Array[Enemy] = []
func register(e: Enemy) -> void:
enemies.append(e)
# no connect() here — call e.take_damage() directly when needed
4. Using disconnect() with the wrong callable reference
Symptom. You call disconnect() before connect() to get a clean slate, but the
error still appears. Or disconnect() prints "Signal not connected" even though you just
connected it.
Cause. In Godot 4, a Callable is matched by both the bound object and the method
name. A new lambda is a different Callable object even if the body is identical —
disconnect() can't find it and does nothing, leaving the old listener in place.
# WRONG — two different Callable objects; disconnect() fails silently:
signal.connect(func(): _handle_hit())
signal.disconnect(func(): _handle_hit()) # not found — old listener stays
Fix. Store the callable in a variable and reuse the same reference for both calls:
var _hit_callable: Callable
func _ready() -> void:
_hit_callable = _on_hit # store the bound method
hitbox.area_entered.connect(_hit_callable)
func _exit_tree() -> void:
if hitbox.area_entered.is_connected(_hit_callable):
hitbox.area_entered.disconnect(_hit_callable)
Named methods (_on_hit, not lambdas) are the safest choice for connections you need
to remove later.
What all four have in common
Every case is the same root problem: connect() was called more than once for the same
(signal, callable) pair. Godot 4 doesn't deduplicate connections by default — each extra
call adds another listener, and every listener fires when the signal emits.
The safe pattern is always: connect once in _ready() or _enter_tree(), disconnect
explicitly in _exit_tree(), and guard with is_connected() when the timing is
uncertain. If you'd rather skip wiring Area2D signals for combat altogether — and
avoid this entire class of bug — that's what Saltmire Hitbox handles internally:
Hitbox2D manages its own connections and tears them down automatically when the node
leaves the tree.