Debounce is a technique that prevents a function or event from firing again until a set amount of time passes. Think of it like a cooldown. If a player clicks a button, debounce ensures they can't trigger it again for 0.5 seconds (or however long you set). Without debounce, a single click might register multiple times accidentally, causing bugs or unfair behavior.

Debounce is one of the most important patterns you'll use in Roblox scripting. Whether you're building combat, handling button clicks, or processing pickups, debounce keeps your code stable and fair.

What Is Debounce?

Debounce uses a boolean flag and time delays to control how often code runs. Here's the core idea:

  • A variable tracks whether the action is currently on cooldown.
  • When the event fires, check the flag first.
  • If cooldown is active, ignore the event.
  • If cooldown is off, run the code and start the cooldown.
  • After the cooldown duration, reset the flag.

The word "debounce" comes from electronics: a switch bounces when pressed, triggering multiple signals. Debouncing filters out those unwanted repeats.

The Simple Pattern

Here's the basic structure you'll use in almost every debounce scenario:

Basic Debounce Pattern
local debounce = false
local cooldownTime = 0.5

local function doSomething()
    if debounce then
        return  -- Exit early if on cooldown
    end

    debounce = true  -- Lock the action
    
    -- Your code here
    print("Action executed!")
    
    wait(cooldownTime)  -- Wait for cooldown duration
    debounce = false  -- Unlock the action
end

-- Call your function
doSomething()

The flow is simple: check the flag, lock it, do work, wait, unlock. Every time someone tries to trigger the action during the wait, the first if statement blocks them.

๐Ÿ’ก Tip โ€” Always set debounce = true before the work happens. If you wait first and then set the flag, two calls might both pass the check.

Why Debounce Matters

Without debounce, players can spam actions and break your game:

  • Button clicks: A player rapidly clicks a buy button and gets charged multiple times or receives multiple rewards.
  • Combat: A player clicks to attack once, but the script triggers damage 5 times in one frame.
  • Pickups: A player collides with a coin, and both the player and coin's touch event fire, applying the effect twice.
  • Teleports: A touch event teleports the player, then they're still touching it and get teleported again.

Debounce solves all these by enforcing a minimum time between triggers.

Debounce With a Cooldown Timer

For more control, use a numeric variable instead of just a boolean. This tracks the exact time you can act again:

Timer-Based Debounce
local lastActionTime = 0
local cooldownDuration = 1  -- 1 second cooldown

local function takeAction()
    local currentTime = tick()  -- Get the current time in seconds
    
    if currentTime < lastActionTime + cooldownDuration then
        return  -- Still on cooldown
    end
    
    -- Action is allowed
    print("Action executed at " .. currentTime)
    lastActionTime = currentTime
    
    -- Your code here
end

-- Test it
takeAction()
wait(0.5)
takeAction()  -- Blocked (still cooling down)
wait(0.6)
takeAction()  -- Allowed (1.1 seconds have passed)

Using tick() is more precise than a boolean. You don't need to explicitly wait; the next call just checks whether enough time has passed. This pattern is great for games where you need to show cooldown UI or track exact timings.

Common Mistakes

Mistake 1: Setting debounce after the wait.

โŒ Wrong
if debounce then return end

print("Action!")
wait(0.5)
debounce = false  -- Too late! Requests during wait() slip through

Mistake 2: Not unlocking the debounce.

โŒ Wrong
if debounce then return end
debounce = true
print("Action!")
-- Missing: debounce = false
-- Now the action is locked forever

Mistake 3: Using a shared debounce for multiple players.

โŒ Wrong
local sharedDebounce = false

local function giveReward(player)
    if sharedDebounce then return end
    sharedDebounce = true
    
    player:WaitForChild("leaderstats").Gold.Value += 10
    
    wait(1)
    sharedDebounce = false  -- Blocks ALL players from getting rewards during this wait!
end

If you're handling multiple players, each player needs their own debounce. Store them in a table keyed by player ID or use a debounce inside the player's script.

๐Ÿ’ก Tip โ€” For per-player debounces, keep a dictionary like local playerDebounces = {} and check playerDebounces[player.UserId].

Real-World Example: Click to Damage

Here's how debounce works in a combat scenario. Say you have a humanoid and a click detector:

Combat Damage Script (Inside Part with ClickDetector)
local part = script.Parent  -- The part this script is in
local clickDetector = part:WaitForChild("ClickDetector")
local humanoid = part.Parent:WaitForChild("Humanoid")  -- Assumes this part is in a model with a humanoid

local debounce = false
local damageAmount = 10
local cooldown = 0.5

local function takeDamage()
    if debounce then
        return
    end
    
    debounce = true
    
    humanoid:TakeDamage(damageAmount)
    print("Hit! Health is now " .. humanoid.Health)
    
    wait(cooldown)
    debounce = false
end

clickDetector.MouseClick:Connect(takeDamage)

Now when a player clicks the part, they trigger damage once per 0.5 seconds maximum. Rapid clicks are ignored. The humanoid can't be damaged again until the cooldown finishes.

If you want this to work for a player's weapon instead, store the debounce on the player or in a module:

Player-Side Attack Script (In StarterPlayer โ†’ StarterCharacterScripts)
local player = game.Players:GetPlayerFromCharacter(script.Parent)
local lastAttackTime = 0
local attackCooldown = 0.6

local UserInputService = game:GetService("UserInputService")

local function attack()
    if tick() - lastAttackTime < attackCooldown then
        return  -- Still on cooldown
    end
    
    lastAttackTime = tick()
    
    print(player.Name .. " attacked!")
    -- Fire a remote or play an animation here
end

UserInputService.InputBegan:Connect(function(input, gameProcessed)
    if gameProcessed then return end
    
    if input.KeyCode == Enum.KeyCode.E then
        attack()
    end
end)

Every time the player presses E, the code checks if 0.6 seconds have passed since the last attack. If not, the attack is blocked. This prevents spam and makes combat feel fair.