CooldownCache - easily create cooldowns on remotes!

Ever needed an easy way to create and manage cooldowns for your remotes to prevent people spamming them? Well, I created this simple but powerful module that does all this work for you in < 5 lines of code, including the setup!

Introducing CooldownCache

So, what does it do?

  • It’s important to add a cooldown system on important remotes you don’t want to spam, but that can get tedious really fast, especially when you have a lot of remotes. This module makes it easy to create and manage a cooldown system on a remote. It’s also pretty versatile and can easily be forked to be used for other things as well, keep that in mind! Just that this version here is designed for remotes. Recommended for server use only, obviously.

How does it work?

  • It works like any other cooldown system, just that this module makes it soooo much easier if you need to do it frequently (which you probably will need to do for good server security). You set it up by making a new cache object with the .new(cooldownTime) constructor in the module. By using the :IsReady(userId) function of the cache object, it checks if the amount of time passed since the last reset of a player’s cooldown in the cache is less than the cooldownTime you specify. If it is, it will return false, and you can use that to make a simple guard clause (shown below). If it is eligible to reset again, the function will return true and the cooldown will automatically reset.

How do I use it?

  • It’s extremely simple to set up and create a cache. This is an OOP-styled module, so it will be easy to set up and will be especially useful when it comes to frequent usage of a cooldown system like this. API and documentation is in the module, so be sure to grab it.

Example of how easy this is to use:

local cache = require(script.CooldownCache)
local remote = game:GetService("ReplicatedStorage").RemoteEvent

local testCache = cache.new(1) -- Set up a cache with a 1 second cooldown

remote.OnServerEvent:Connect(function(player)
		
	if not testCache:IsReady(player.UserId) then -- This is all you need to do to get the cooldown in place. 
		warn("Not ready") 				     -- Simple guard clause, and it resets automatically if it
		return 							-- returns true, so you don't have to reset the cooldown manually.
	end	
		
	print("YUP") -- Will only print if it gets past the guard clause
		
end)

I have the client fire the remote every .1 second. Results:


Place file:
CooldownCacheTesting.rbxl (24.0 KB)

Source Code
-- CooldownCache
-- Easily manage remote cooldowns with this OOP-styled module! Quick and efficient. 

-- rek_kie
-- 12/20/20

-- API:

-- function cooldown.new(number timeframe)
	-- Creates a new cache object. The number you supply as a parameter will act as the cooldown time 
	-- for the cache. This object automatically sets up PlayerAdded as well as Removing connections, and 
	-- you can completely wipe the cache object with cache:Clear()

-- function cache:IsReady(userId)
	-- This is the main function you'll actually be using, this handles the functionality of the cooldown.
	-- This checks if the player is eligible to 
	-- It automatically resets the cooldown if this returns true, so it's literally as simple as this to 
	-- use with a remote (below)


-- == EXAMPLE == --

--[[
	local cache = require(script.CooldownCache)
	local remote = game:GetService("ReplicatedStorage").RemoteEvent

	local testCache = cache.new(1)

	remote.OnServerEvent:Connect(function(player)
		
		if not testCache:IsReady(player.UserId) then -- This is all you need to do to get the cooldown in place. 
			warn("Not ready") 				     -- Simple guard clause, and it resets automatically if it
			return 							-- returns true, so you don't have to reset it manually.
		end	
		
		print("YUP")
		
	end)
]]--


-- function cache:Clear()
	-- This completely clears the cache and all connections and wipes everything from memory.
	-- Should only be used if you don't need a cache anymore.

-- Enjoy this simple, but extremely powerful thing I made :)
-- Hope this becomes of use to you.


local players = game:GetService("Players")
local http = game:GetService("HttpService")

local activeCooldowns = {}
local cooldown = {}

cooldown.__index = cooldown

function cooldown.new(timeframe)
	
	local self = {}
	
	local id = http:GenerateGUID(false) 
	
	self.time = timeframe 
	self.name = id -- make a UUID
	self.cache = {}
	
	local function onPlayerAdded(player)
		if not self.cache[tostring(player.UserId)] then 
			self.cache[tostring(player.UserId)] = tick()
		end
	end
	
	local function onRemoving(player)
		if self.cache[tostring(player.UserId)] then 
			self.cache[tostring(player.UserId)] = nil
		end
	end
	
	self.connections = {
		players.PlayerAdded:Connect(onPlayerAdded), 
		players.PlayerRemoving:Connect(onRemoving)
	}
	
	for _, player in ipairs(players:GetPlayers()) do
		coroutine.wrap(onPlayerAdded)(player)
	end
	
	activeCooldowns[id] = self
	
	return setmetatable(self, cooldown)
	
end

function cooldown:IsReady(userId)
	
	local last = self.cache[tostring(userId)]
	
	if last then 
		if tick() - last < self.time then 
			return false 
		end
	end
	
	self.cache[tostring(userId)] = tick() -- automatically reset 
	
	return true 
	
end

function cooldown:Clear()
	
	for index, con in ipairs(self.connections) do -- clear from memory
		if self.connections[index] then
			self.connections[index]:Disconnect()
			self.connections[index] = nil
		end
	end
	
	if activeCooldowns[self.name] then -- clear from the master table
		activeCooldowns[self.name] = nil
	end
end


return cooldown

You can get it here:

https://www.roblox.com/library/6112410666/CooldownCache

20 Likes

For “cooldowns” that go below 0.1 seconds, I have a module that instead limits the rate per second a remote is triggered with an almost identical API:

RateLimiter.lua

Since Roblox can pack several RemoteEvent triggers together within that short amount of time, the server will receive them as if they happened at the same time. The difference of my module is that it will sometimes allow rapid successive triggers server-side when rate is greater than 1 - that might not be desirable in some cases, but is quite desirable in many other cases too.

12 Likes

Wow - didn’t even know this existed, definitely going to check this out

Nice :+1:

2 Likes

What’s the benefit of using this instead of:

local enabled = true
game:GetService("ReplicatedStorage").Remote.OnServerEvent:Connect(function(plr)
    if enabled == false then
        return
    end
    enabled = false
    -- code
    wait(4)
    enabled = true
end)
2 Likes

Well for one, that enabled variable will share across all players. This module allows you to achieve what you just tried to but with much less code.

7 Likes

This here

#1 That isn’t even a table so that variable would be for every single player
#2 When you actually do have to do this, it will get tedious to constantly set up tables, track time, and playerAdded/Removing connections so I made this module that does the work for you

Also, the wait(4) isn’t a good idea, there’s a possibility it can wait more than that. You should be tracking time passed since calls using tick() or os.clock() for more accuracy

2 Likes

Great contribution, will definitely be using this.

Kind of nitpicking here but wouldn’t it be more convenient to pass the player object when invoking the IsReady method for the cooldown? Was this done out of preference or optimization?

function cooldown:IsReady(Player)
	local last = self.cache[tostring(Player.UserId)]
	
	if last then 
		if tick() - last < self.time then 
			return false 
		end
	end
	self.cache[tostring(Player.UserId)] = tick() -- automatically reset 

	return true 
end
2 Likes

This was done so that it can be used even when a player object isn’t available (although that’s highly unlikely). It’s really up to preference, it doesn’t matter that much really

2 Likes

Can you elaborate on this a little more? Theoretically the player object should always be available as long as the player that fired the remote is still in-game.

1 Like

Worded that wrong

By player object isn’t available I mean when you’re using this for something outside of players. For example, I just implemented this in an attack system for my AI enemies in a side project to debounce the damage (obviously it was a fork)

1 Like

To add on to this, instead of using names I generate my own ID’s for each NPC and pass them in

1 Like

I’d recommend using os.clock() for more precise calculations.

Like he mentioned it’s for everybody in the server; A code redeeming system for example is something that you need to be very careful about, because people can spam it and get duplicated stuff.

Can you give an example on how function cooldown.new(number timeframe) can be used?

Very useful actually, as I never got to make a module like this.

Current Benefits I see

  • Accurate
  • Useful for anticheats
  • Prevents Client Lag from gaining an advantage
1 Like