In short
Keep a variable that says "busy". At the top of the handler, return if it is set; otherwise set it, do the work, wait, and clear it. For something each player can use, keep a table of the last time each player used it, lastUse[player] = os.clock(), and return when os.clock() - lastUse[player] is below the cooldown. Find the player from the touching part with Players:GetPlayerFromCharacter(hit.Parent).
- A single debounce for the whole part blocks everyone else too. For rewards, debounce per player.
- Timestamps (
os.clock) are simpler and safer thantask.waitinside the handler. - Any RemoteEvent a player can fire needs a cooldown on the server: a cheater can fire it thousands of times.
Get the script
Touched debounce script with a cooldown per player
A Script inside a part that gives coins on touch, once every few seconds per player, however many body parts touch it.
-- Script inside a Part named CoinPad.
-- Gives coins on touch, at most once every COOLDOWN_SECONDS per player.
local Players = game:GetService("Players")
local pad = script.Parent :: BasePart
local COOLDOWN_SECONDS = 5
local COINS = 10
local lastPaid: { [Player]: number } = {}
pad.Touched:Connect(function(hit: BasePart)
local character = hit.Parent
local player = if character and character:IsA("Model") then Players:GetPlayerFromCharacter(character) else nil
if not player then
return -- an NPC or a loose part
end
local now = os.clock()
if now - (lastPaid[player] or 0) < COOLDOWN_SECONDS then
return -- this player was paid recently
end
lastPaid[player] = now
local leaderstats = player:FindFirstChild("leaderstats")
local coins = leaderstats and leaderstats:FindFirstChild("Coins")
if coins and coins:IsA("IntValue") then
coins.Value += COINS
end
end)
Players.PlayerRemoving:Connect(function(player: Player)
lastPaid[player] = nil -- do not keep players who left
end)
RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts
Where to put it
Workspace
CoinPad Part new
CoinPad Script new
Settings you can change
COOLDOWN_SECONDSIn the script- How long one player waits before the pad pays them again. Default:
5. COINSIn the script- Coins per touch. Default:
10.
Coins go into a Coins IntValue in leaderstats, which the leaderboard guide creates.
What a cheater can and cannot doThe cooldown is kept on the server per player, so touching faster or with more body parts never pays more.
Type-checked with luau-lsp strict against Roblox's API definitions on 5 November 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.
Prompt for RoCode
Make the CoinPad part give 10 coins to a player who touches it, at most once every 5 seconds per player, on the server, using leaderstats Coins, and clean up when players leave.
On this page
Why Touched fires so many times
A character is made of many parts: feet, legs, the torso, accessories. Touched fires once for each part that starts touching, and again when a part stops and starts touching as the character walks or bobs. Roblox's own debounce page puts it simply: Touched can fire several times in quick succession for what looks like one contact. A handler with no debounce runs for every one of them.
-- Script inside a Part. Step on it once and count the prints.
local pad = script.Parent :: BasePart
pad.Touched:Connect(function(hit: BasePart)
print("Touched by", hit.Name)
end)
Press Play and walk onto the part: Output shows LeftFoot, RightFoot, LeftLowerLeg and more, all in one step. That is what a debounce filters.
Step 1: The classic debounce
The simplest version uses one true or false for the whole part. While it is true, every touch is ignored. This suits things that should happen once for everyone, such as a trap that fires and resets.
-- Script inside a Part named Trap.
-- Drops a ball when anyone steps on it, then waits 3 seconds before it can fire again.
local trap = script.Parent :: BasePart
local RESET_SECONDS = 3
local busy = false
trap.Touched:Connect(function(hit: BasePart)
if busy then
return
end
local humanoid = hit.Parent and hit.Parent:FindFirstChildOfClass("Humanoid")
if not humanoid then
return -- not a character: do not start the cooldown for a loose part
end
busy = true
local ball = Instance.new("Part")
ball.Shape = Enum.PartType.Ball
ball.Size = Vector3.new(4, 4, 4)
ball.Position = trap.Position + Vector3.new(0, 30, 0)
ball.Parent = workspace
game:GetService("Debris"):AddItem(ball, 5)
task.wait(RESET_SECONDS)
busy = false
end)
Set busy before anything that waits, and check for a character first, so a falling brick does not lock the trap. Debris:AddItem removes the ball after 5 seconds without another wait in your code.
Step 2: A cooldown per player
For rewards, one player's cooldown must not block another. Keep a table keyed by player with the time each one last got paid. os.clock() returns seconds with high precision, so the check is one subtraction and the handler never waits.
-- Script inside a Part named CoinPad.
-- Gives coins on touch, at most once every COOLDOWN_SECONDS per player.
local Players = game:GetService("Players")
local pad = script.Parent :: BasePart
local COOLDOWN_SECONDS = 5
local COINS = 10
local lastPaid: { [Player]: number } = {}
pad.Touched:Connect(function(hit: BasePart)
local character = hit.Parent
local player = if character and character:IsA("Model") then Players:GetPlayerFromCharacter(character) else nil
if not player then
return -- an NPC or a loose part
end
local now = os.clock()
if now - (lastPaid[player] or 0) < COOLDOWN_SECONDS then
return -- this player was paid recently
end
lastPaid[player] = now
local leaderstats = player:FindFirstChild("leaderstats")
local coins = leaderstats and leaderstats:FindFirstChild("Coins")
if coins and coins:IsA("IntValue") then
coins.Value += COINS
end
end)
Players.PlayerRemoving:Connect(function(player: Player)
lastPaid[player] = nil -- do not keep players who left
end)
Removing players on PlayerRemoving stops the table from growing for the whole life of the server. If the pad should pay only once per visit, never clear lastPaid[player] until they leave, or use a set: if paid[player] then return end.
Step 3: A cooldown module for everything
Pads, abilities, shop buttons and remotes all need the same check. Put it in one ModuleScript and each script asks it: "may this player do this now?"
-- ModuleScript in ServerScriptService named Cooldown.
-- Cooldown.try(player, "Dash", 2) returns true at most once every 2 seconds for that player and action.
local Players = game:GetService("Players")
local Cooldown = {}
local lastUse: { [Player]: { [string]: number } } = {}
function Cooldown.try(player: Player, action: string, seconds: number): boolean
local now = os.clock()
local actions = lastUse[player]
if not actions then
actions = {}
lastUse[player] = actions
end
local last = actions[action]
if last and now - last < seconds then
return false
end
actions[action] = now
return true
end
function Cooldown.remaining(player: Player, action: string, seconds: number): number
local actions = lastUse[player]
local last = actions and actions[action]
if not last then
return 0
end
return math.max(seconds - (os.clock() - last), 0)
end
Players.PlayerRemoving:Connect(function(player: Player)
lastUse[player] = nil
end)
return Cooldown
Every server Script that requires it shares the same table, because a ModuleScript runs once per server. So a cooldown set by your pad script is seen by your shop script, which is what you want for an action name they share.
Step 4: Debounce RemoteEvents on the server
A cooldown in a LocalScript only stops honest players. A cheater can call FireServer directly as often as they like, so every remote that does something needs its own check on the server. With the module:
-- Script in ServerScriptService named DashServer.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
type CooldownModule = {
try: (Player, string, number) -> boolean,
remaining: (Player, string, number) -> number,
}
local Cooldown = require(ServerScriptService:WaitForChild("Cooldown")) :: CooldownModule
local DASH_COOLDOWN = 2
local dashRemote = ReplicatedStorage:WaitForChild("Dash") :: RemoteEvent
dashRemote.OnServerEvent:Connect(function(player: Player)
if not Cooldown.try(player, "Dash", DASH_COOLDOWN) then
return -- too soon: ignore silently
end
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
if root and root:IsA("BasePart") then
root.AssemblyLinearVelocity += root.CFrame.LookVector * 60
end
end)
Keep a matching cooldown in the LocalScript too, so the button greys out and honest players see why nothing happened; the server one is the one that counts. Ignoring early requests silently is fine. Kicking for them is not: lag can bunch up two honest presses. The RemoteEvents guide covers the other checks a remote needs.
Common debounce mistakes
| Mistake | What happens | Fix |
|---|---|---|
Setting busy = true after a wait | Several touches get through before it is set. | Set it first, then do the work. |
One busy for a reward pad | While one player is paid, nobody else can be. | Use a cooldown per player. |
| Starting the cooldown for any part | A rolling brick locks the pad for everyone. | Check for a character or player first. |
An error before busy = false | The part stays locked forever. | Use timestamps, which need no reset, or wrap the work in pcall. |
| Cooldown only in the LocalScript | Cheaters fire the remote as fast as they like. | Check it on the server. |
| Never clearing the table | It grows with every player who ever joined. | Clear entries on PlayerRemoving. |
Common errors and fixes
Open the Output window (Window menu, then Output) before you press Play.
It still fires several times
The debounce check runs after something slow, or each touch is a different player. Put the check at the very top of the handler, and print the player's name to see who is triggering it.
It works once and never again
The handler set busy = true and then errored or returned before setting it back. Look for a red line in Output, or switch to the timestamp version in Step 2, which never gets stuck.
attempt to index nil with leaderstats
The touching part was not a player's character, so player is nil. Return early when GetPlayerFromCharacter gives nil. See attempt to index nil.
Build it with RoCode
If you would rather not fix every pad by hand, RoCode can do it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin edits the scripts in your Explorer.
My coin pads pay out many times per step. Add a 5 second cooldown per player to every Touched reward script in my game, kept on the server, and add a server cooldown to my Dash RemoteEvent.
- Can find every Touched handler and remote with Search Scripts before it changes anything
- Edits the scripts in place and is told to compile-check each one with Check Script
- Sets an undo point before and after each batch of changes, so Ctrl+Z steps back one batch at a time
With plugin 3.9.15 or newer RoCode can control a test character that walks onto pads, but it checks only what it chose to test, so step on each pad yourself too. How RoCode connects to Studio.
Questions
What is a debounce in Roblox?
A check that ignores an event for a short time after it last ran, so one touch or one click does the action once. Usually a boolean or a timestamp.
Why does Touched fire multiple times?
Each body part that touches fires it, and parts touch again as the character moves. A single step can fire it ten times or more.
How do I make a cooldown in Roblox?
Save os.clock() when the action runs, and next time return early if less than the cooldown has passed. Keep one saved time per player for things each player uses.
Should I use task.wait in a debounce?
It works for the simple version, but timestamps are better: the handler never pauses, nothing can get stuck locked, and you can tell the player how long is left.
Sources and further reading
- Roblox Creator Docs: Debounce patterns. Touched firing several times and the debounce fix
- Roblox Creator Docs: BasePart.Touched
- Roblox Creator Docs: Players:GetPlayerFromCharacter
- Roblox Creator Docs: Securing the client-server boundary. rate limiting what clients send
How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 5 November 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.