Godot Timer Node Basics: Cooldowns, Spawn Intervals, and Time Limits

Created: 2026-09-22Last updated: 2026-10-10

Build three Timers for a cooldown, a spawn interval, and a time limit, and learn how to start and stop them and display the remaining time.

Anything that waits a set number of seconds, like an attack cooldown, an enemy spawn interval, or a stage time limit, is built with a Timer node. A Timer, once started, counts a set number of seconds and emits the timeout signal when it finishes counting.

Use it without knowing how it behaves, and mashing a button bypasses the cooldown, or the time-limit label jumps in the last second. Enemies may also keep spawning during a pause.

This article lines up three Timers, a cooldown, a spawn interval, and a time limit, to cover how their settings differ and how to stop them.

Three Timers for attack, spawning, and the time limit running at once, each counting down its own remaining time

What You'll Learn

  • start() before it finishes counting throws away what's left and counts over from the beginning
  • start(0.5) rewrites the wait time itself
  • time_left is 0 before starting and after stop(). Display it with ceili()
  • stop() throws away what's left, while paused stops and keeps it

Tested with: Godot 4.7

Sponsored

How a Timer works (timeout and one_shot)

A Timer does only one thing. Start it and it begins counting a set number of seconds, and when it finishes counting, it emits the timeout signal. Connect a function, as in $Timer.timeout.connect(_on_timeout), and that function is called every time it finishes counting.

Starting it makes the remaining time go down, and when it finishes counting, timeout fires. With one_shot on it stops, with it off it counts again from the start

one_shot decides what happens after it finishes counting. With it on, it stops there. With it off, it starts counting again from the beginning.

In other engines: it plays a role close to Unreal Engine's Set Timer by Event (turn on Looping to repeat). You can think of it as a node version of the waits you'd write in Unity with Invoke() or a coroutine.

If you just want to wait once and continue, you don't need a Timer node: await get_tree().create_timer(1.0).timeout is enough. When to use which is covered in await and Coroutines. What follows is about using the in-between state of a wait from buttons and labels. Reading what's left, stopping partway, branching on whether a button was pressed. If you need those three, place a Timer node.

Settings for the three Timers

Rather than memorizing a list of properties, it's faster to line up three with different uses.

Attack and time limit have one_shot on and run once, only spawning has one_shot off and repeats, and only spawning has autostart on

The attack cooldown (the wait after using something before you can use it again; 0.5 seconds here) and the time limit (9 seconds) have one_shot on and stop after counting once. Only the spawn interval (2 seconds) has it off and counts over again and again. The same timeout means something different for each of the three: for the attack, "ready for the next one"; for spawning, "the next enemy"; for the time limit, "the end."

autostart is a switch that starts the timer automatically when it enters the scene. For things like spawning that "start moving once placed," it's all you need. The time limit is started from code because it's nice to keep the start of the stage as a single line. If starting at the same moment the scene loads is fine, autostart works for this one too.

Calling start() partway through

Try writing the attack cooldown so that every press attacks and calls start().

# Only input the UI didn't use arrives here. ui_accept is mapped to Space and Enter out of the box
func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("ui_accept"):
		attack_label.text = "Slash"
		attack_cooldown.start()     # counts over from the beginning every time you press
Pressing every 0.2 seconds sets what's left back to 0.5 seconds each time, so timeout never fires on this mashing timeline

Mash it, and Slash appears on every press. The cooldown isn't working at all. What's more, while you keep pressing at 0.0, 0.2, and 0.4 seconds, what's left goes back to 0.5 seconds on each press. It never finishes counting, so timeout never fires. The signal to set AttackLabel back to ... never comes either.

What stops it is the button side. Add a check so it only swings when the timer is free.

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("ui_accept") and attack_cooldown.is_stopped():
		attack_label.text = "Slash"
		attack_cooldown.start()
Attacking only when is_stopped() lets the press at 0.0 s through, passes over 0.2 and 0.4 s, and lets 0.6 s through again

is_stopped() is true in three situations: before it's ever been started, after stop(), and after a one_shot timer has finished counting. While it's stopped with paused, which keeps what's left, it stays false. The press at 0.0 seconds goes through, and 0.2 and 0.4 seconds pass right over. The next one to go through is the press at 0.6 seconds, after it has finished counting.

Put the check on the button side. If you check is_stopped() inside the function connected to timeout, a one_shot timer has already stopped by the time you get there. It's always true, so the check is useless.

Sponsored

start()'s argument (overwriting wait_time)

You can pass a number of seconds to start(). It isn't an addition. It overwrites the length.

attack_cooldown.start(0.5)     # wait_time is rewritten to 0.5
attack_cooldown.start()        # still 0.5 seconds from now on. It doesn't go back to the Inspector value

Even if the Inspector says 2.0, once start(0.5) runs, that node's wait_time is 0.5. If you want it to run on the Inspector's value, don't pass an argument in the first place.

On the other hand, rewriting wait_time while it's running doesn't change what's left in the current count. What changes is the length the next time it starts counting.

Displaying the remaining time (time_left)

You can read the remaining time with time_left. There's just one thing to watch when displaying it.

Right after starting, int shows 8.98 s as 8 while ceili shows 9, and in the last 0.9 s int shows 0 while ceili shows 1
func _process(_delta: float) -> void:
	time_label.text = "Time left %d" % ceili(stage_limit.time_left)

int(time_left) rounds down, so 8.98 right after starting shows as 8, and the last 0.9 shows as 0. You barely see 9, and it jumps to 0 in the last second. With ceili() (a function that rounds a decimal up to an integer), you can count from 9 down to 1.

One more thing: before starting and after stop(), time_left returns 0. The label shows 0 before starting too, so start updating the display once the stage has begun.

Leave the end check to timeout. Check time_left <= 0 inside _process and it's true before starting as well as every frame after it ends.

Ways to stop it (stop, paused, pause)

There are three ways to stop it, and they treat the remaining time differently.

stop() throws away what's left and goes to 0, paused keeps what's left and stops, and a tree pause stops it along with everything around it
  • stop(): throws away what's left and stops. time_left becomes 0, and timeout doesn't fire
  • paused = true: stops while keeping what's left. Set it back to false and it continues counting from there
  • get_tree().paused = true: stops the whole tree. The Timer stops too

For a node you want to keep running during a pause, set Process → Mode in the Inspector to Always. In code, that's process_mode = Node.PROCESS_MODE_ALWAYS. process_callback switches whether the remaining time goes down in the per-frame processing or the physics processing. Even set to Physics, it stops during a pause. These are two separate switches.

If enemies keep spawning during a pause, the cause is usually one of two things. Either spawning runs on get_tree().create_timer(), or the Timer's process_mode is Always, including when inherited from a parent. A throwaway timer from create_timer() keeps running during a pause by default. To stop it, pass a second argument as in create_timer(seconds, false), or leave periodic things to a Timer node. How to build a pause itself is in Pause and Process Mode.

During a tree pause, the Timer node stops, while create_timer() keeps counting by default and enemies spawn
Sponsored

Hands-On: A Stage With a 0.5 s Attack, 2 s Spawns, and a 9 s Limit

Attack intervals in an action game, spawns in tower defense, the countdown in a time attack—managing waits comes up in every genre. You'll run three at once and see how their settings differ.

① What to place

Create one new scene and save it as main.tscn. Set this scene under Project Settings → Application → Run → Main Scene.

Main (Node2D)                 main.gd
├── AttackCooldown (Timer)
├── SpawnInterval (Timer)
├── StageLimit (Timer)
└── Hud (VBoxContainer)       position (40, 40). A container that stacks its children vertically
    ├── TimeLabel (Label)
    ├── SpawnLabel (Label)
    └── AttackLabel (Label)

Set up the three Timers in the Inspector like this. Leave them named Timer, Timer2, and Timer3 and you'll lose track of which is which.

NodeWait TimeOne ShotAutostart
AttackCooldown0.5OnOff
SpawnInterval2.0OffOn
StageLimit9.0OnOff
# main.gd
extends Node2D

@onready var attack_cooldown: Timer = $AttackCooldown
@onready var spawn_interval: Timer = $SpawnInterval
@onready var stage_limit: Timer = $StageLimit
@onready var time_label: Label = $Hud/TimeLabel
@onready var spawn_label: Label = $Hud/SpawnLabel
@onready var attack_label: Label = $Hud/AttackLabel

var spawned := 0
var finished := false

func _ready() -> void:
	attack_cooldown.timeout.connect(_on_attack_ready)
	spawn_interval.timeout.connect(_on_spawn)
	stage_limit.timeout.connect(_on_stage_end)
	spawn_label.text = "Enemies 0"
	attack_label.text = "..."
	stage_limit.start()                # the stage starts here

func _process(_delta: float) -> void:
	if not finished:
		time_label.text = "Time left %d" % ceili(stage_limit.time_left)

func _unhandled_input(event: InputEvent) -> void:
	if finished:
		return
	if event.is_action_pressed("ui_accept") and attack_cooldown.is_stopped():
		attack_label.text = "Slash"
		attack_cooldown.start()        # don't accept the next one until it's free

func _on_attack_ready() -> void:
	attack_label.text = "..."

func _on_spawn() -> void:
	spawned += 1
	spawn_label.text = "Enemies %d" % spawned

func _on_stage_end() -> void:
	finished = true
	spawn_interval.stop()              # stop spawning once it's over
	attack_cooldown.stop()             # stop() doesn't fire timeout, so change the display here
	time_label.text = "Time left 0"
	attack_label.text = "Finished"

② See how the three overlap

Before pressing F5, check what happens during the 9 seconds.

SpawnInterval has autostart, so it starts counting the moment it enters the scene. The first enemy appears after 2 seconds. If you want one to appear right at the start, call _on_spawn() once in _ready(), then leave the rest to the timer.

StageLimit starts with the start() in _ready() and fires timeout at 9 seconds. At that point _on_stage_end() stops spawning and the cooldown, and also turns away input.

③ Run it and check

Run with F5. Check three things. If space doesn't respond, click the game screen once.

Right after starting it shows Time left 9 and Enemies 0, while mashing it shows Slash, and after 9 seconds it changes to Time left 0, Enemies 4, and Finished

The first is the time limit. Right after starting it shows Time left 9, Enemies 0, and ..., and the number drops about once a second. If you display it with int(), it changes to 8 right after starting.

The second is the attack. Once it says Slash, pressing any number of times passes right over for about 0.5 seconds. Try removing the is_stopped() check, and every press goes through. While you keep pressing before 0.5 seconds pass, it stays at Slash and never goes back to ..., because you keep making it count over from the beginning before it finishes.

The third is spawning and the end. The Enemies count goes up at about 2, 4, 6, and 8 seconds, and at about 9 seconds it becomes Time left 0 and Finished. The enemies stop at 4, and pressing space after that does nothing. The three Timers ran all the way through, each with its own length and way of stopping.

Two points.

  • Place one Timer per wait: count everything with one, as in "spawn on the 4th tick, end on the 18th," and the moment you change the attack speed, spawning and the end both break. Keep them separate with names, and later you only need to change the numbers in the Inspector
  • Hand out the end from one place: _on_stage_end() raises finished, stops spawning and the cooldown, and turns away input. stop()-ing the cooldown sets is_stopped() back to true, so what actually stops input is finished

Troubleshooting

SymptomCauseFix
timeout never firesYou're calling start() every frame inside _process(). Like mashing, it counts over from the beginning every timeStart it only once, where the trigger happens, such as _ready() or the button handling
A Timer created in code doesn't run, and an error appearsYou called start() before add_child(). It can't start outside the treeadd_child() it to a parent that's in the scene tree, then start()
Stopping it partway doesn't run the cleanup written for timeoutstop() doesn't fire timeoutCall the same cleanup function from the side that stops it too
The cooldown only works the first time, and you can attack freely from the second onAttackCooldown's One Shot is off. It doesn't stop after it finishes counting, so is_stopped() never goes back to trueTurn One Shot on (it's off by default)

Bonus: Good to Know First

  • autostart turns itself off once started: after it starts automatically, the value at runtime goes back to false. The setting saved in the scene stays as-is, so the next time the scene opens it starts automatically again
  • start(0) doesn't go off immediately: pass 0 or less and the argument is ignored, starting with the current wait_time. wait_time itself can't be 0 either. The Inspector's minimum is 0.001 seconds
  • For just counting, doing it yourself is fine: add up delta (the seconds since the last frame) in _process and you can count seconds. Think of Timer as that plus stopping, remaining time, and timeout
  • Keep the owner close to the user: in the hands-on, the three were placed under Main so you could see them side by side. The attack cooldown really belongs as a child of whatever attacks, so it travels with it

Summary

  • A Timer counts a set number of seconds: when it finishes counting it fires timeout. Turn one_shot off and it counts again from the beginning
  • start() partway through counts over from the beginning: it throws away what's left. Put the check on the calling side
  • start(0.5) rewrites wait_time: even the next start() with no argument uses that length
  • While stopped, time_left is 0: before starting and after stop(). Round the display up with ceili()
  • Throw it away, or keep it: stop() throws away what's left, paused keeps it. To keep running during a pause, set Process → Mode to Always

When "I want to stop it partway" or "I want to show what's left" comes up, that's the cue to place a Timer. Is there anywhere in _process right now where you're subtracting seconds yourself?

Read nextNext in the learning path
GDScript

Managing Data Across Scenes with Autoload

Read this note
Godot Notes in this section104