How to check if a player is spamming a remote too fast?

Why are you using a remote to handle sword hitting? Wouldn’t the Touched event be better in this case?

Right now I am not worried about hit detection exploitation. I am just worried about them spamming the remote.

1 Like

I believe client hit detection is better.

EDIT: Not to say I would do hit verification on the server.

1 Like

Not trying to sound harsh but that is entirely wrong. A player can change ANY local variable on their client. Any variable in a localscript can and will be manipulated by an exploiter.

7 Likes

I don’t quite understand what you mean but whatever.

Depending on how your sword swings, a debounce might be all you need to do, to ensure the player can only deal damage x times/second.

local debounce = false
remote.OnServerEvent:Connect(function(plr, stuff)
    if debounce then return end
    debounce = true
    --stuff
    wait(.5)
    debounce = false
end)

Because events don’t arrive at the server at the same relative times (to each other) as when they were fired on the client, a leaky-bucket type of rate limiter is the most sound approach. Basically, you count events received, per-player, per-event (increment the counter when each event is received), and you decrement the counter periodically by an amount that is just more than the expected average rate by some safety margin. Then, you set a threshold (the “bucket” capacity), and if the counter exceeds this, you react accordingly (stop processing their events, flag the user, whatever…) This type of system gives you a time buffer, so that when you have a scenario like evently spaced (in time) events from the client hitting the server in a burst (due to internet routing time differences), it doesn’t trigger the overflow like if you just counted events per second without any buffering.

15 Likes

Is that right? I was under the impression only variables in ModuleScripts required by the client could be changed if they please. How do you change a variable in a LocalScript while it’s running?

1 Like

Can you explain what this means?

Exploits have the debug library implemented which allows the use of functions such as debug.setupvalue, allowing them to change almost if not all variables they please in any localscript (or modulescript).

1 Like

I edited my reply to explain it a bit. But you’ll probably get a much better explanation here: https://en.wikipedia.org/wiki/Leaky_bucket

1 Like

This wouldn’t effect the server though.

I can give you a simple, concrete example, as that wiki article is actually kind of verbose.

Suppose that your game lets a player swing their sword as fast as 3 times per second, and that attacking faster than this is only possible with an exploit. You might first think to have an attack counter for each player, that is checked and reset once per second on the server. The trouble with this is that it’s too simple and doesn’t account for variability in internet latency: I could legitimately attack a player at max attack speed for 2 seconds, a total of 6 attacks each 1/3 of a second apart (on my client). But, due to unequal transit times across the internet for my events, or a back up of event processing on the server, the server might see this as 2 attacks in the first second, and 4 attacks in the next 1-second interval. 4 attacks per second is above the max attack speed, but events from my client were valid!

So, what you do instead is something like this: Each attack you get from my client increase my attack counter. Once per second, you subtract up to 3 from my counter. Then after this subtraction, you compare the value of my attack counter against some threshold, which is a bit of an arbitrary safety margin. Suppose the threshold in this case is also set to 3 (which will tolerate a transient of up to 2x normal attack rate). What happens is my first 2 attacks make my counter 2. On the next check 1 second later, you try to subtract up to 3 from this, so my counter is now 0 (counter clamps at min of 0). 0 is less than the threshold of 3, so I’m still fine. On the next check, you process my 4 attacks, setting my counter to 4. The following second (during which I made no further attacks), you again start by subtracting 3, reducing my counter to 1. 1 is less than the threshold, so I’m still within the allowable tolerances.

Now, suppose I exploit and start sending 5 attacks per second. Well, you can see that adding 5 but subtracting 3 on each check will quickly overflow the bucket. My counter will go like this: 5-3=2(OK) 2+5-3=4(overflow!)

Basically, buffering the input in this manner makes it tolerant of events sometimes bunching up, yet still possible to validate the average rate of events that do naturally happen in bursts (like sword attacks). The tuning parameters for this kind of system are the bucket size (the threshold), and max allowed rate, and how often you check. Realistically, exploiters tend to just spam events at many times the normal rates, so you can be pretty generous with bucket size.

73 Likes

You can also upgrade the leaky buckets by instead of decrementing every so often, keep track of the last time the bucket was decremented. When a request comes in, decrement the bucket by the time since the last decrement divided by the time per a unit of leakage. This can be done with floating point values (like Lua numbers) or integers and there isn’t a need to perform rounding operations.

This should be very cheap too, nice suggestion Emily!

8 Likes

A simple way is to have a server sided dictionary, containing booleans stored by online user ids.

local debounces = {}

On every remote call, check if the boolean exists / is true. if debounces[userId] then…
if it does not exist or is false, set it to true, and set it to false after your function is done.

2 Likes

The drawback with this kind of system is that it has no tolerance for varying latency, it uses server arrival times, but the timings of events is not preserved through transmission over the internet. Valid event timings from a client can easily be rejected, which is frustrating for the player, especially if the client is doing any kind of prediction (like visually firing a shot, for which a projectile is then never created on the server).

In most cases, you want to make sure the average rate of client events is within sane limits, but in a way that tolerates variation in the arrival times of individual events. It’s not trusting the client completely, just giving the system enough margin of error to not start falsely rejecting valid input because of variations in ping.

2 Likes

Can’t you use os.time() difference checks instead then?

Yeah, and you can use tick() as well. This wouldn’t be as efficient though, because you’d need to implement a double sided queue. As requests came in and were accepted, you’d push a timestamp onto the top of the queue. To check if too many requests were coming in, you’d pop the oldest requests off the bottom of the queue until you reached one that was within a memory threshold (say, 10 seconds). You’d then compare the current number of requests in the queue to determine if there have been more than a desired amount in 10 seconds.

1 Like

I wouldn’t recommend it. If you trust client timestamps, you just give other types of exploiters (cheaters) something very obvious to tamper with.

For detecting true “spamming” of remotes, average rates at the server end are all you really care about, this isn’t the same use case as detecting players who are doing things like using skills before they go off cooldown, exploiters who are spamming remotes are generally not trying to cheat, they are trying to overload a remote that does something expensive (like cloning models from storage) in order to crash the server.

1 Like

You can use os.time() on the server :stuck_out_tongue: and just have a hashed table with last uses stored by userid.

I don’t see what use this would be on its own. What would you do with this data?