Check how many times a remote has been fired in a time range (Sanity Check)

Hey so, I’m trying to secure my remotes so exploiters can’t just spam them. I’m having some trouble to set up what is called a sanity check. I looked into the forum how others managed to do it, but I can’t seem to understand how it works. I tried making my own, which would enable and disable a value in the player, but I then realized it’s for the whole server and if someone will fire a remote, legitimately, they would get banned. Can someone tell me how you can check how many times a player has fired a remote in an amount of time on server?

2 Likes

When the OnServerEvent is fired, store a tick() in the table with the player’s name or user ID. Then let’s say the RemoteEvent is fired again, if there is a stored player’s tick in the table, compare it with the new tick (tick() - playersOldTick). Now, is the time too less or too high? If it’s too less, it means the remote event was fired so quick.

1 Like

Thanks, I’ll try to play around with it, but it seems like your solution would definitely work.

2 Likes

Here’s what I do

local EventDebounce = {}

 Event.OnServerEvent:Connect(function(player, args)
 if os.time() - (EventDebounce[player.Name] or 0) >= Cooldown then
    EventDebounce[player.Name] = os.time() -- again, tick() would be much better in this case
    print("performing action") 
    end
 end)
 -- you could use tick() for more accuracy if you want

You could even explicitly call wait(n) from the client before firing so that if on the server, the event was fired too quick, the player could be kicked (knowing the only time the remote would fire too quick would be if an exploiter were spamming.)

 -- client
 local debounce
 if not debounce then debounce = true
    Event:FireServer(args)
    wait(1.5)
    debounce = not debounce
 end

-- server

local Event = game:GetService("ReplicatedStorage").RemoteEvent

EventDebounce = {}

Event.OnServerEvent:Connect(function(player, args) 
local lastFired = EventDebounce[player.Name]
if lastFired then -- if this isn't the first time the event was fired

	if tick() - lastFired  < 1.4 then print("too fast")
	   player:Kick("Not so fast")
	   return 
	  end
    end
    EventDebounce[player.Name] = tick()
    print("performing action") 
end)

Edit: It will not be a problem if the LocalScript gets deleted, see my reply below.

10 Likes

The only problem is that the exploiter has full control of their client, they can delete the local script, but it can still prevent inexperienced exploiters. Thanks for the detailed reply.

1 Like

That’s not actually a problem, even if the exploiter deletes the script or fires through an alternate method, we’re still logging every event on the server with tick(), and if the time between events fired is too short, the player gets kicked.

The reason I did that part on the client was so that there would be a delay between firing- and if there’s too short of a delay (meaning the LocalScript was edited), the script on the server still verifies whether the amount of time between the last event fired is greater than the cooldown specified on the client.

Remember that not all players are exploiters, and you’re going to need some sort of debounce on the client too.

2 Likes

Yeah, because they can’t have access to server scripts. Thanks.

1 Like

Here’s a pretty basic implementation that should work pretty well. Just start each Remote.OnServerEvent listener with

if not NetworkHandler(Player, RemoteName, MaxRequestsPerSecond) then return end

to throttle excess requests.

local DEFAULT_MAX_REQ_PER_SECOND = 20 --Default maximum requests per remote event per second
local RemoteCalls = {}
function NetworkHandler(Player, RemoteName, MaxRequestsPerSecond)
	if not MaxRequestsPerSecond then MaxRequestsPerSecond = DEFAULT_MAX_REQ_PER_SECOND end
	if not RemoteCalls[Player.UserId] then RemoteCalls[Player.UserId] = {} end
	RemoteCalls[Player.UserId][RemoteName] = (RemoteCalls[Player.UserId][RemoteName] or 0) + 1
	return RemoteCalls[Player.UserId][RemoteName] <= MaxRequestsPerSecond 
end

--Clearing up memory as players leave
game.Players.PlayerRemoving:Connect(function(Player)
	if RemoteCalls[Player.UserId] then
		RemoteCalls[Player.UserId] = nil
	end
end)

--Remote1 will allow up to 20 requests per player per second
Remote1.OnServerEvent:Connect(function(Player, data)
	if not NetworkHandler(Player, "Remote1") then return end
end)
--Remote2 uses the optional max request parameter, allowing 3 requests per player per second
Remote2.OnServerEvent:Connect(function(Player, data)
	if not NetworkHandler(Player, "Remote2", 3) then return end
end)

--Clear everyone's network calls every second
while wait(1) do
	for i,v in pairs(RemoteCalls) do
		RemoteCalls[i] = {}
	end
end
8 Likes