Quick answer: The most common cause is connecting the same signal to the same callback more than once. This happens when connect() is called in _ready() on a node that is repeatedly added and removed from the scene tree, or in _process() by mistake.

Here is how to fix duplicate signal connections multiple calls Godot. Your signal callback is supposed to fire once, but it fires two, three, or even more times every time the signal emits. Health gets deducted multiple times per hit. Items get added to the inventory in duplicate. Score increments are doubled. The culprit is almost always duplicate signal connections — and they accumulate silently because Godot does not warn you when you connect the same signal to the same callback more than once.

The Symptom

A method connected to a signal executes more times than expected. You press a button once, but the connected callback runs twice. An enemy dies once, but the score updates three times. The problem gets worse over time or as you reload scenes.

extends Node2D

func _ready():
  $Button.pressed.connect(_on_button_pressed)

func _on_button_pressed():
  print("Pressed!")  # Prints 1x first time, 2x after scene reload, 3x after...

The first time the scene loads, everything works correctly. But if the scene is reloaded, or the node is removed and re-added to the tree, the callback count increments. Each scene load adds another connection without removing the previous one.

What Causes This

Cause 1: Connecting in _ready() on a node that re-enters the tree. When a node is removed from the scene tree and added back (for example, when changing scenes and returning, or when using object pooling), _ready() is called again. Each call adds a new connection. Godot allows multiple identical connections on the same signal, so each connect() call stacks another one on top of the existing connections.

Cause 2: Connecting inside _process() or other per-frame methods. This is less common but more catastrophic. If a connect() call accidentally ends up in a method that runs every frame, you will accumulate 60+ connections per second. The callback will fire dozens of times per signal emission.

Cause 3: Connecting to autoload signals from instanced scenes. Autoloads persist across scene changes. If an instanced node connects to an autoload signal in _ready() but does not disconnect when the scene is freed, the connection survives. When the next scene instance connects, there are now two connections. This is the most common source of duplicate connections in projects that use global signal buses.

The Fix

Step 1: Guard connections with is_connected(). Before connecting, check if the connection already exists:

func _ready():
  if not $Button.pressed.is_connected(_on_button_pressed):
    $Button.pressed.connect(_on_button_pressed)

func _on_button_pressed():
  print("Pressed exactly once!")

Step 2: Use CONNECT_ONE_SHOT for signals that should fire once. If a connection should only trigger a single time and then automatically disconnect, pass the one-shot flag:

# Connection auto-disconnects after the first emission
func _ready():
  $AnimationPlayer.animation_finished.connect(
    _on_death_animation_finished,
    CONNECT_ONE_SHOT
  )

func _on_death_animation_finished(anim_name: StringName):
  if anim_name == "death":
    queue_free()

Step 3: Disconnect signals in _exit_tree(). Pair every connection with a disconnection when the node leaves the tree. This prevents stale connections from accumulating across scene loads:

extends Node2D

func _ready():
  GameEvents.enemy_spawned.connect(_on_enemy_spawned)
  GameEvents.wave_started.connect(_on_wave_started)

func _exit_tree():
  # Clean up all connections to the autoload
  if GameEvents.enemy_spawned.is_connected(_on_enemy_spawned):
    GameEvents.enemy_spawned.disconnect(_on_enemy_spawned)
  if GameEvents.wave_started.is_connected(_on_wave_started):
    GameEvents.wave_started.disconnect(_on_wave_started)

func _on_enemy_spawned(enemy):
  print("Enemy spawned: ", enemy.name)

func _on_wave_started(wave_num: int):
  print("Wave ", wave_num, " started")

Step 4: Debug connection counts at runtime. If you suspect duplicate connections, print the connection list to see how many exist:

# Debug helper: count connections on a signal
func _debug_connections(obj: Object, signal_name: String):
  var connections = obj.get_signal_connection_list(signal_name)
  print("Signal '", signal_name, "' has ", connections.size(), " connections:")
  for conn in connections:
    print("  -> ", conn["callable"])

Why This Works

The is_connected() guard prevents the same Callable from being connected twice. Godot’s signal system allows multiple connections of the same Callable to the same signal — this is by design for advanced use cases where you genuinely want a callback invoked multiple times. But for most game logic, this is never the intent. The guard makes the connect call idempotent.

CONNECT_ONE_SHOT tells the signal system to automatically disconnect the Callable after it has been invoked once. This is perfect for animation callbacks, cutscene triggers, and any other one-time event. The connection cannot accumulate because it self-destructs after first use.

Disconnecting in _exit_tree() matches the lifecycle pattern of _ready(). Since _ready() is called when a node enters the tree, _exit_tree() is the natural counterpart for cleanup. This is especially critical for connections to autoloads and other persistent objects that outlive individual scene instances.

“If your callback fires twice, you connected twice. Godot does not deduplicate connections for you.”

Related Issues

If the signal is connected once but the callback never fires, the issue is not duplication but a connection that is silently broken. See fixing signals that are connected but whose callbacks never execute.

If you are managing signals across different scenes and connections are lost entirely when switching scenes, you may need a global signal bus pattern. See fixing cross-scene signal communication.

For issues where signals pass the wrong number of arguments to callbacks, which can also manifest as unexpected behavior alongside duplicate calls, see fixing signal argument mismatches.

Connect once. Disconnect on exit. Guard everything else.