Rate limiter module for Remotes

Hello there!
Exploiters often try to spam your remotes to see if they exceed the limit the server can handle and causing it to crash.
In general, it is good practice to limit the rate of your remotes to prevent that behavior. This module should make this process very easy for you.

What it does:

This module I made allows you to easily set a maximum request rate for your RemoteEvents or RemoteFunctions. With this module, you can simply ignore requests if your set maximum rate is exceeded by the user.

How it works:

The module counts every request made by the player through a specified remote and determines whether or not the request should be processed by the listener.

Easy implementation:

local FloodCheck = require(game.ServerScriptService.RemoteFloodCheck) -- Reference the module
MyEvent.OnServerEvent:Connect(function(Player)
	if FloodCheck:Check(Player, MyEvent, 10) then --< This returns true if the request should be processed.
		-- Process the request
	end
end

Features:

  • Module logs the player that fires the remote and the remote that was fired itself. This means that if player A throttles a remote, player B can still use it.
  • The module changes behavior depending on the maximum rate that is set by you (or default rate if left unspecified). If you have a rate set to higher than 1 per second, let’s say 10 per second, the script will let the first 10 requests through regardless of time in between and then block all the consecutive requests that were made in that second. However, if you have, let’s say a maximum rate of 1 request per 5 seconds, it blocks all the requests that were made within the first 5 seconds after the first request.
  • The module can be used on multiple scripts listening to the same remote at once without them counting as duplicates (and then filling up the number of requests the player can make). This is useful when dealing with packages or clones of scripts that all listen to the same remote.
  • The module has a Unique_Identifier feature that allows you to assign separate MaxRates to different scripts that are listening to the remote at the same time. This can be used to throttle more resource-intensive code more harshly than easier to run more critical code.

Source code (with extra documentation on how to implement):

Code (to be used in a ModuleScript)
-->> Settings
local DefaultMaxRatePerSec = 10
local DefaultIdentifier = "Identifier"
-->>

-- Made by XandertjeKnal
--[[ Documentation:
This module can be used to make sure rates of Remotes are not exceeded.

FloodCheck:Check(Player_Object, Event, RatePerSec, Unique_Identifier) -- Will return false if the limit is exceeded, otherwise will return true.

- Player_Object
Reference the player that is making the request.

- Event
Reference the event that you are listening to.

- RatePerSec:
RatePerSec > 1
If the RatePerSec variable x is set higher than 1, it will allow x requests in the first second after the first request was made. E.g. if RatePerSec is set to 5, a player can send 5 consecutive requests in any time that is less than a second, for example within the first 200ms even. All other requests in that second will be blocked. After that second, the request count will be reset.
RatePerSec <= 1
If the RatePerSec variable x is set lower or equal to 1, it will only allow a consecutive request (1/x) seconds after the first request. E.g. if RatePerSec is set to 0.2, a player has to wait (1/0.2) = 5 seconds after sending the first request before any consecutive requests will be processed. All other requests in between will be blocked.
The RatePerSec should not be changed after the first request has been checked.
If left nil, the DefaultMaxRatePerSec will be used.

- Unique_Identifier:
This variable is optional to use and can most often be left nil. You only have to use this if there are multiple scripts listening to the same RemoteEvent/RemoteFunction.
E.g. 2 scripts are listening to an 'ExplodeTNT' event. You can set one Unique_Identifier to '1' and the other to '2'. If you are using clones of the same script (or packages) listening to the same event then you may want to use a random number or string that is randomized only once(!) per script as the Unique_Identifier instead.
This identifier is used to prevent multiple checks from filling up the flood check.
You could in theory also use this identifier to have different Rates for different scripts listening to the same remote. Example usage: throttle resource-intensive scripts more than the game-critical scripts, even if they are listening to the same event.

- Example usage:
local FloodCheck = require(game.ServerScriptService.RemoteFloodCheck) -- Reference this module
MyEvent.OnServerEvent:Connect(function(Player)
	if FloodCheck:Check(Player, MyEvent, 10) then
		-- Process the request
	end
end
--]]

local FloodCheck = {}

local Requests = {}

function FloodCheck:Check(Player_Object, Remote, RatePerSec, Unique_Identifier)
    local Rate = RatePerSec or DefaultMaxRatePerSec
	local Identifier = Unique_Identifier or DefaultIdentifier
	
	if not Requests[Player_Object] then
		Requests[Player_Object] = {}
		local connection
		local function PlayerLeft(player)
			if player == Player_Object then
				Requests[Player_Object] = nil
				connection:Disconnect()
			end
		end
		connection = game.Players.PlayerRemoving:Connect(PlayerLeft)
	end
	if not Requests[Player_Object][Remote] then
		Requests[Player_Object][Remote] = {}
	end
	if not Requests[Player_Object][Remote][Identifier] then
		Requests[Player_Object][Remote][Identifier] = {}
	end
	
	if Rate > 1 then
		if Requests[Player_Object][Remote][Identifier]["Count"] then
			local TimeElapsed = tick() - Requests[Player_Object][Remote][Identifier]["StartTime"] 
			if TimeElapsed >= 1 then
				Requests[Player_Object][Remote][Identifier]["Count"] = 1
				Requests[Player_Object][Remote][Identifier]["StartTime"] = tick()
				return true
			else
				Requests[Player_Object][Remote][Identifier]["Count"] = Requests[Player_Object][Remote][Identifier]["Count"] + 1
				return Requests[Player_Object][Remote][Identifier]["Count"] <= Rate
			end
		else
			Requests[Player_Object][Remote][Identifier]["Count"] = 1
			Requests[Player_Object][Remote][Identifier]["StartTime"] = tick()
			return true
		end
	end
	if Rate <= 1 then
		if Requests[Player_Object][Remote][Identifier]["LastTime"] then
			local TimeElapsed = tick() - Requests[Player_Object][Remote][Identifier]["LastTime"]
			if TimeElapsed >= (1/Rate) then
				Requests[Player_Object][Remote][Identifier]["LastTime"] = tick()
				return true
			else
				return false
			end
		else
			Requests[Player_Object][Remote][Identifier]["LastTime"] = tick()
			return true
		end
	end
end

return FloodCheck
Link to module

https://www.roblox.com/library/5151966367/Remote-Rate-Limiter-Module-Flood-Check

59 Likes
game.Players.PlayerRemoving:Connect(function(player)
    if player == Player_Object then
	   Requests[Player_Object] = nil
    end
end)

You should probably end up disconnecting this event, going to cause a memory leak. Cool contribution though!

5 Likes

This is a contribution that actually has a real-world use, nice job, I usually implement this on my own, but having a module will be nicer to add and to modify.

3 Likes

Hoo boy, now this is what I like to see.

2 Likes

Thank you for the feedback, the code has now been updated!

3 Likes

Hi @XandertjeKnal, thank you for sharing your creation with us!

I have a question and is wondering if this could be used on the Clientside as well as the first level of defense?

I was thinking maybe using it with a Remote wrapper that will call FireServer, InvokeServer

I usually do sanity checks on both Client and Server and I can see the potential for this.

2 Likes

The module can be used on the client, yes. Keep in mind that this will only limit your own LocalScripts from firing remotes. It will not prevent exploiters from firing them. To prevent that, you will have to use the module on the server in scripts that listen to the remotes, so the requests can be blocked there.

Not sure why but, could you explain why this would cause a memory leak?

1 Like

Because the connection is not disconnected, it will be called more times than it should. If you keep firing the function the event is going to be ran another time.

I can’t really explain this too well, this thread is a good read.

2 Likes

I thought roblox already monitors this. If a client over calls the remote then that player will get throttled not the others?

1 Like

afaik it shouldn’t impact other clients, it doesn’t make sense to compromise other users when they aren’t involved.

Although Roblox does monitor this it’s for a different purpose and does a different thing, which that’s not the point of having this in your game, it’s to filter out Exploiters from Legitimate Clients.

1 Like

Roblox has no way to know if a remote is being ‘over-throttled’ or not; some remotes are supposed to be fired a lot, some are not.

The point of throttling the remotes yourself is to reduce the stress on the server that may be caused when clients trigger a resource-heavy task multiple times (and too frequently!).

The point @RuizuKun_Dev made is also valid. When, for instance, an exploiter fires a remote too many times, it should not negatively impact the experience for the other players (by having the server crash or by disabling the function attached to the remote completely). This was the main motivation behind making and publishing this module.

1 Like

This post has often been quoted in the context of other networking problems.
I recently wrote this reply for someone that goes more in-depth on ping, lag, and on how you could tackle some of the other networking problems related to guns in an FPS game:

Unless I misunderstand, isn’t this kind of completely pointless? Anybody can limit players from proceeding in the event with an if statement (and not having to wrap in a pointless function abstraction that uncessarily uses self :wink: )

To actually rate limit, i.e block the server from responding to a certain players request at all, is far different. Because as it stands if that player is exploiting and firing the remote hundreds of times a second, this will do nothing besides stop the server code from executing, in fact because of your module implementation you may end up causing server lag.

Yes, you indeed seem to misunderstand.

Indeed, this module limits the code on the server from executing if a rate is exceeded, therefore reducing stress on the server. This practice is very common and I recommend that you look more into the subject.

This claim is not true at all, all my module does is relatively inexpensive, e.g. manipulating a table. (As you can see in the source code.)

therefore reducing stress on the server.

This doesn’t reduce stress on the server, sure, it’ll stop the code you want to execute from executing, but you don’t seem to know what I mean. What I mean is, if they fire that remote hundreds of times, that event will still fire hundreds of times, that if statement will still conditionally check hundreds of times. Nothing has changed besides the extent to which the player proceeds to. And it’s already too far.

This claim is not true at all, all my module does is relatively inexpensive, e.g. manipulating a table. (As you can see in the source code.)

I’m pretty sure I said the wrong thing on this part, while you may not cause server lag per se, wrapping this in a, again uneeded function call that also doesn’t use self but has it there anyway. It will be more expensive than literally just having only an if statement.

So instead of making wrappers for something that doesn’t actually save any resources, (if anything, the opposite effect compared to standard implementation of this) I’d suggest you instead make a tutorial advising people on this practice of stopping remote abuse.

2 Likes

Can you make RemoteFunction version of this module, please? Nice module, though!

The module works for both RemoteEvents and RemoteFunctions!
In the source code, there are detailed instructions on how to use my module + code examples.

1 Like

I think you’re confused about the expense of function calls - i.e. the lack thereof of any actual cost.

What you’re suggesting is sanity checks (I think?) - those should be included on top of a module like this. Using catch all solutions, like a rate limiting wrapper, is good practice because it acts as a safeguard against accidental failure to implement proper controls (the risk of accidentally allowing a DoS-style attack is increased by relying on sanity checks on every single remote, rather than wrapping them in a module which sorts it for you).

The cost to something like this is minimal, if not plain negligible, and the benefit is pretty clear from a security and performance perspective.

4 Likes

It wasn’t clear from here if I could check if the player is being throttled from a LocalScript (to for instance notify them of this) without it limiting the amount of uses a player gets.

To be more precise, I’m thinking of adding a sort of “Panic Button” function to a custom chat script, and using this to not only prevent spamming remote events but also implement the delay of 20 seconds, or (0.1 allowed times per second)