Simple Remote Event Throttling Module

What's the use case?

Have you ever created a remote function / event and wanted to prevent how quickly players can call it? Then this module is for you!

This module is incredibly simple to use- it only has one public function:

function PlayerThrottler.createThrottler(delayTime: number?): (Player) -> (boolean)

This returns a function that is intended to be called inside the OnServerEvent function that you wish to throttle on a per-player basis.

Copy & Paste the code below in a new module script (server-side) called PlayerThrottler:

-- PlayerThrottler
-- Author: velstorm

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

local Throttlers = {}

local CLEANUP_THROTTLERS_DELAY = 15
local DEBUG = false

type ThrottleEntry = {
	PlayersLastCalledTime: {[Player]: number},
	ThrottleDelay: number,
}

function shouldThrottle(player: Player, id: string): boolean
	assert(player:IsA("Player"), "[PlayerThrottler]: Parameter player must be of type Player.")
	assert(Throttlers[id], "[PlayerThrottler]: Paramater functionKey (" .. id .. ") must be registered with registerThrottler first.")

	local thisThrottleEntry: ThrottleEntry = Throttlers[id]

	local thisPlayerLastCalledTime: number = thisThrottleEntry.PlayersLastCalledTime[player]

	if not thisPlayerLastCalledTime then
		if DEBUG then
			print("[PlayerThrottler]: " .. player.Name .. " called " .. id .. " for the first time.")
		end

		thisThrottleEntry.PlayersLastCalledTime[player] = time()
		return false
	end

	if time() > thisPlayerLastCalledTime + thisThrottleEntry.ThrottleDelay then
		if DEBUG then
			print("[PlayerThrottler]: " .. player.Name .. " called " .. id .. " without throttling.")
		end

		thisThrottleEntry.PlayersLastCalledTime[player] = time()
		return false
	else
		if DEBUG then
			warn("[PlayerThrottler]: " .. player.Name .. " call got throttled for " .. id .. ".")
		end

		return true
	end
end

local PlayerThrottler = {}

----- Public Functions -----

function PlayerThrottler.createThrottler(delayTime: number?): (Player) -> (boolean)
	assert(delayTime ~= nil, "[PlayerThrottler]: Parameter delayTime cannot be nil")
	
	local guid = HttpService:GenerateGUID()
	
	local newThrottleEntry: ThrottleEntry = {
		PlayersLastCalledTime = {},
		ThrottleDelay = delayTime
	}
	
	Throttlers[guid] = newThrottleEntry
	
	if DEBUG then
		print("[PlayerThrottler]: A new throttler is registered with id " .. guid .. ".")
	end
	
	return function(player: Player)
		return shouldThrottle(player, guid)
	end
end

----- Initialization -----

local function cleanupThrottlers()
	for _, tbl: ThrottleEntry in Throttlers do
		for player: Player, lastCalledTime in tbl.PlayersLastCalledTime do
			if time() > lastCalledTime + tbl.ThrottleDelay then
				tbl.PlayersLastCalledTime[player] = nil
			end
		end
	end
	
	if DEBUG then
		print("[PlayerThrottler]: Throttling table was cleaned. New tbl: ", Throttlers)
	end
end

-- We dont want to remove player from all entries on leaving since this may 
-- allow the player to call a function quicker than a throttle time specified 
-- if they join back to the same server quickly enough.
coroutine.wrap(function()
	while true do
		task.wait(CLEANUP_THROTTLERS_DELAY)
		cleanupThrottlers()
	end
end)()

return PlayerThrottler

Example:

The code below shows the remote event “DoThingRemote” being limited to one call every five seconds for each player.

-- Server Script

local DoThingRemote = ReplicatedStorage.DoThingRemote

-- Require the PlayerThrottler you created.
local PlayerThrottler = require(ServerScriptService.PlayerThrottler)

-- Get a new throttler function that returns true if the event doesn't need to be throttled.
-- In this example, the throttle debounce time per-player is 5 seconds.
local shouldThrottle = PlayerThrottler.createThrottler(5)

DoThingRemote.OnServerEvent:Connect(function(player)
    -- shouldThrottle(player) returns true if the player has called this remote event in the last 5 seconds.
    if shouldThrottle(player) then 
        return
    end

    -- Your code

end)

Just wanted to make this since I think it’s a common pattern. If it’s convenient for you to use then it’s for you. If not, then it’s not for you!

Please let me know if you have any comments/suggestions. I’ll fix bugs as soon as I am aware.

Thanks!