Saltmire
Practical Godot 4 tutorials — game feel, saving, and shipping polish fast.

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.

Built this the long way once too many times. Saltmire Hitbox does it as a drop-in tool: https://saltmire.itch.io/saltmire-hitbox