Quick answer: You are creating a new Tween every frame in _process, which restarts the animation from the current value before it can finish. Create the tween once on a trigger event, store the reference in a variable, and let it run to completion.

You write a tween that fades a label from invisible to opaque over half a second. The label flickers, hovers near zero opacity, then snaps back to invisible the moment the tween “finishes.” You stare at the code for an hour and the math is fine. The problem is not the tween itself — it is when and how often you are creating it.

The Symptom

You set up something like a fade-in, scale-up, or position lerp using create_tween().tween_property(...), and one of these things happens:

The property animates briefly but never reaches its target value. The property animates correctly but instantly snaps back to the starting value as soon as the tween finishes. The animation looks broken on every other frame, like it is fighting itself. The finished signal fires far too often, sometimes thousands of times in a second.

All four of these symptoms have the same root cause.

What Causes This

In Godot 4, create_tween() returns a fresh Tween every time you call it. The tween starts running on the next frame and continues independently of any prior tween. If you put create_tween() inside _process or _physics_process, you are creating a new tween every single frame. Each new tween reads the current property value as its starting point, schedules an animation toward your target, and gets immediately superseded by the next frame’s tween.

The result looks like the property “snaps back” because the new tween’s starting value is wherever the property happened to be on the most recent frame, and the new tween’s end value is the same target you keep passing in. The animation never makes net progress because each frame restarts it.

A second related cause is calling queue_free() on the tween’s parent node, or letting the tween go out of scope without storing a reference. Without a reference, the tween is garbage collected and the property freezes wherever it stopped.

The Fix

Step 1: Move the tween creation out of _process.

Create the tween in a trigger event — a button press, a signal callback, or a state change — not in a per-frame loop.

# BAD: creates a new tween every frame
func _process(delta):
    var tween = create_tween()
    tween.tween_property($Label, "modulate:a", 1.0, 0.5)

# GOOD: create the tween once on a trigger
func show_label():
    if _tween and _tween.is_running():
        _tween.kill()
    _tween = create_tween()
    _tween.tween_property($Label, "modulate:a", 1.0, 0.5)

Step 2: Store the tween reference.

Declare a member variable for the active tween. This lets you query its state, kill it cleanly when you start a new one, and prevent the garbage collector from finalizing it mid-animation.

extends Control

var _fade_tween: Tween

func fade_in(duration: float = 0.5) -> void:
    if _fade_tween:
        _fade_tween.kill()
    _fade_tween = create_tween()
    _fade_tween.set_ease(Tween.EASE_OUT)
    _fade_tween.set_trans(Tween.TRANS_CUBIC)
    _fade_tween.tween_property(self, "modulate:a", 1.0, duration)
    _fade_tween.finished.connect(_on_fade_finished, CONNECT_ONE_SHOT)

func _on_fade_finished() -> void:
    modulate.a = 1.0  # Belt-and-braces: lock the final value

Step 3: Bind the tween to a node.

By default, a Tween created with create_tween() runs on the SceneTree and survives even if the node is freed. If the node disappears mid-tween, the property update tries to write to a freed object and Godot logs an error. Use bind_node so the tween dies cleanly with its target.

_fade_tween = create_tween().bind_node(self)
_fade_tween.tween_property(self, "modulate:a", 1.0, 0.5)

When Two Tweens Fight Each Other

If you have multiple tweens running on the same property — for example, a fade and a flash effect both touching modulate — the most recently started tween wins on each frame. They will visually fight each other. The fix is to combine them into a single tween using set_parallel(true) and tween the same property only once, or use composition (animate modulate.a in one tween and modulate.r in the other).

Verifying the Fix

Add a print statement to your trigger function and one to the finished signal callback. With the bug, the print statements fire every frame. With the fix, they fire exactly once per trigger. If you still see snap-back after fixing the loop, check whether anything else is writing to the property — an AnimationPlayer, a Theme, or a child node’s set_modulate on the parent.

“A Godot tween should be created on an event, not in a frame. If your tween code is anywhere inside _process, you have a bug whether you have noticed it yet or not.”

Related Issues

For Godot 4 tween migration issues from Godot 3, see Godot tween not working in Godot 4. For animation reset issues with AnimationPlayer instead of Tween, check Godot animation not resetting after stop. If your tween fires on a node that has been freed, see Godot object freed while signal pending.

Use Godot 4’s built-in Tween.is_running() to guard against creating duplicate tweens. It is the cheapest one-line fix for the “snap back” bug.