DEV Community

saltmire
saltmire

Posted on Originally published at saltmire.github.io

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'.
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.


If you'd rather drop this in than build it, Saltmire Hitbox does it as a ready-made tool: https://saltmire.itch.io/saltmire-hitbox

Originally published at https://saltmire.github.io/godot-4-signal-already-connected.html

Top comments (0)