Debounce is one of the most important patterns in Roblox scripting. Without it, your code can fire multiple times when you only wanted it once. You click a button and damage the enemy five times. You press a key and jump thirty times. Events triggered repeatedly cause chaos.

The good news: debounce is simple once you understand it. You use a boolean flag (a true/false variable) to temporarily block a function from running, then turn it back on after a delay.

The Debounce Pattern

Debounce works in three steps:

  1. Check if the action is allowed (is the flag true?)
  2. If yes, run the code and set the flag to false (block it)
  3. Wait a moment, then set the flag back to true (allow it again)

Here's the simplest example:

Basic Debounce Pattern
local debounce = true

button.Activated:Connect(function()
	if debounce then
		debounce = false
		
		-- Your code runs here
		print("Button pressed!")
		
		wait(1) -- Wait 1 second
		debounce = true
	end
end)

When the player clicks the button:

  • If debounce is true, the code runs and debounce becomes false.
  • The next click during the 1-second wait will skip the code entirely (debounce check fails).
  • After 1 second, debounce turns back to true, and the button works again.

Debounce in Damage Systems

This is where debounce really shines. If your sword touches an enemy, you only want to deal damage once, not every frame the sword overlaps the enemy.

Debounce for Damage
local debounce = false
local damage = 25

sword.Touched:Connect(function(hit)
	local humanoid = hit.Parent:FindFirstChild("Humanoid")
	
	if humanoid and not debounce then
		debounce = true
		
		humanoid:TakeDamage(damage)
		print("Hit for " .. damage .. " damage!")
		
		wait(0.5) -- Prevent hitting the same enemy twice in 0.5 seconds
		debounce = false
	end
end)

Without debounce, the Touched event might fire 10+ times per second while the sword is touching the enemy. With debounce, you deal damage only once every 0.5 seconds.

💡 Tip — Use a shorter debounce time (0.1 to 0.5 seconds) for damage, and a longer one (1–3 seconds) for abilities or special moves.

Debounce with Functions

If you have a function you want to debounce, pass the debounce check into it:

Function-Based Debounce
local debounce = true

local function useAbility()
	print("Ability used!")
	-- Your ability code here
end

userInputService.InputBegan:Connect(function(input, gameProcessed)
	if gameProcessed then return end
	
	if input.KeyCode == Enum.KeyCode.E and debounce then
		debounce = false
		
		useAbility()
		
		wait(2) -- 2-second cooldown
		debounce = true
	end
end)

Per-Player Debounce

In some games, you need different debounce timers for each player. A shared debounce flag blocks everyone. Instead, store a debounce state for each player:

Per-Player Debounce
local debounces = {} -- Dictionary to store debounce state per player

button.Activated:Connect(function(player)
	-- Initialize debounce for this player if not set
	if debounces[player] == nil then
		debounces[player] = true
	end
	
	if debounces[player] then
		debounces[player] = false
		
		print(player.Name .. " pressed the button!")
		
		wait(1)
		debounces[player] = true
	end
end)

Now each player has their own 1-second cooldown timer. Player A can press the button while Player B is waiting.

Advanced: Debounce with tick()

The wait() approach works, but there's a more precise method using tick(), which returns the current time in seconds:

Debounce with tick()
local lastActivated = 0
local cooldown = 1 -- 1 second cooldown

button.Activated:Connect(function()
	local currentTime = tick()
	
	if currentTime - lastActivated >= cooldown then
		lastActivated = currentTime
		
		print("Button pressed!")
		-- Your code here
	end
end)

This approach doesn't block execution with wait(). Instead, it checks if enough time has passed since the last successful activation. It's cleaner for high-frequency events (like raycasts or damage checks that run every frame).

💡 Tip — Use tick() debounce in loops or physics callbacks. Use boolean debounce for single events like button clicks.

Common Debounce Mistakes

Forgetting to set debounce back to true: Your code runs once and never again because debounce stays false forever. Always pair debounce = false with debounce = true after the wait.

Using global debounce for multiple events: If you have three buttons sharing one debounce flag, pressing any button blocks all of them. Give each event its own debounce variable.

Wait time too short: If your debounce is 0.01 seconds, the player can click twice almost instantly. Test different timings to feel right.

Debounce in the wrong place: The debounce check must be at the start of the event listener, before the actual code. Otherwise, the code runs before it's blocked.

Where to Place Debounce Scripts

  • LocalScripts in buttons: Place a LocalScript inside a TextButton or ImageButton in StarterGui to debounce button clicks.
  • Server scripts for damage: Put debounce logic in a Script inside the weapon part (like a sword handle) so all players respect the same cooldown.
  • LocalScripts for player input: Debounce keyboard or mouse events in a LocalScript in StarterPlayer > StarterCharacterScripts or StarterPlayerScripts.

Quick Recap

  • Debounce prevents a function from running multiple times in a short period.
  • Use a boolean flag that toggles off and on around your code.
  • Always set it back to true after the wait, or it stays blocked forever.
  • For damage or continuous checks, consider using tick() instead of a boolean.
  • Test different cooldown times until the gameplay feels right.