How to make a Rust high ping kick plugin (Oxide timer and permission)
Checks every online player's average network ping on a repeating timer and kicks anyone who stays above the configured limit for three checks in a row. A permission exempts chosen players, and a chat warning is sent before the kick.
A single player on a bad connection can drag down hit registration and make firefights feel broken for everyone. Rust does not expose a built-in maximum ping setting, so admins either script it or leave bad connections online until players complain. This plugin samples Network.Net.sv.GetAveragePing(connection) for every connected player on a repeating timer, counts how many checks in a row the value stays over the limit, and kicks the player once the count reaches the configured number (three by default). A single sample under the limit resets the counter, so a short lag spike never kicks anyone. An exemption permission covers admins, VPN users or anyone you want to protect. Before the kick, the player gets a chat warning and a kick reason so they know why they are being removed.
The plugin code
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Ping Kick", "RustPluginGenerator", "1.0.0")]
[Description("Warns and kicks players whose average ping stays above a limit for several checks")]
public class PingKick : RustPlugin
{
private const string PermExempt = "pingkick.exempt";
private const string PermAdmin = "pingkick.admin";
private PluginConfig config;
private Timer scanTimer;
private readonly Dictionary<ulong, int> streaks = new Dictionary<ulong, int>();
private class PluginConfig
{
[JsonProperty("Max Ping")]
public int MaxPing = 350;
[JsonProperty("Checks Before Kick")]
public int ChecksBeforeKick = 3;
[JsonProperty("Check Interval Seconds")]
public float CheckInterval = 30f;
[JsonProperty("Warn Before Kick")]
public bool WarnBeforeKick = true;
[JsonProperty("Kick Reason")]
public string KickReason = "High ping";
}
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null) throw new Exception();
}
catch
{
PrintWarning("Configuration file is invalid, using defaults");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["Kick.Warning"] = "Warning: your ping is above {0} ms. You will be kicked if it does not drop. Try reconnecting.",
["Kick.Reason"] = "Kicked: average ping above {0} ms",
["Command.NoPermission"] = "You do not have permission to use this command.",
["Command.Check"] = "Ping scan complete. Tracked: {0}. Highest ping: {1} ms. Limit: {2} ms.",
["Command.Status"] = "Ping check every {0}s, limit {1} ms, {2} consecutive failures trigger a kick.",
["Usage"] = "Usage: /pingkick check | /pingkick status"
}, this);
}
private string Msg(string key, string userId = null, params object[] args) => string.Format(lang.GetMessage(key, this, userId), args);
private void Init()
{
permission.RegisterPermission(PermExempt, this);
permission.RegisterPermission(PermAdmin, this);
}
private void OnServerInitialized()
{
if (config.MaxPing < 50) config.MaxPing = 50;
if (config.ChecksBeforeKick < 1) config.ChecksBeforeKick = 1;
if (config.CheckInterval < 1f) config.CheckInterval = 1f;
scanTimer = timer.Every(config.CheckInterval, Scan);
}
private void Unload()
{
if (scanTimer != null)
{
scanTimer.Destroy();
scanTimer = null;
}
streaks.Clear();
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null) return;
streaks.Remove(player.userID.Get());
}
private void Scan()
{
if (config == null) return;
foreach (var player in BasePlayer.activePlayerList)
{
if (player == null || player.IsNpc || !player.IsConnected || player.IsSleeping()) continue;
if (permission.UserHasPermission(player.UserIDString, PermExempt)) continue;
var connection = player.net?.connection;
if (connection == null) continue;
int ping = Network.Net.sv.GetAveragePing(connection);
if (ping <= 0) continue;
ulong id = player.userID.Get();
if (ping <= config.MaxPing)
{
if (streaks.ContainsKey(id)) streaks[id] = 0;
continue;
}
int streak;
streaks.TryGetValue(id, out streak);
streak++;
streaks[id] = streak;
if (streak < config.ChecksBeforeKick) continue;
streaks[id] = 0;
KickHighPing(player);
}
}
private void KickHighPing(BasePlayer player)
{
if (player == null || !player.IsConnected) return;
if (config.WarnBeforeKick)
{
player.ChatMessage(Msg("Kick.Warning", player.UserIDString, config.MaxPing));
}
string reason = Msg("Kick.Reason", null, config.MaxPing);
player.Kick(reason);
}
[ChatCommand("pingkick")]
private void CmdPingKick(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
player.ChatMessage(Msg("Command.NoPermission", player.UserIDString));
return;
}
string sub = args != null && args.Length > 0 ? args[0].ToLowerInvariant() : "";
if (sub == "check")
{
int highest = 0;
int tracked = 0;
foreach (var p in BasePlayer.activePlayerList)
{
if (p == null || p.IsNpc || !p.IsConnected) continue;
var connection = p.net?.connection;
if (connection == null) continue;
int ping = Network.Net.sv.GetAveragePing(connection);
if (ping > highest) highest = ping;
if (streaks.ContainsKey(p.userID.Get())) tracked++;
}
player.ChatMessage(Msg("Command.Check", player.UserIDString, tracked, highest, config.MaxPing));
return;
}
if (sub == "status")
{
player.ChatMessage(Msg("Command.Status", player.UserIDString, config.CheckInterval, config.MaxPing, config.ChecksBeforeKick));
return;
}
player.ChatMessage(Msg("Usage", player.UserIDString));
}
}
}Hooks used: OnServerInitialized, OnPlayerDisconnected
Config file
Created automatically on first load at oxide/config/PingKick.json.
{
"Max Ping": 350,
"Checks Before Kick": 3,
"Check Interval Seconds": 30,
"Warn Before Kick": true,
"Kick Reason": "High ping"
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/pingkick check | Runs one immediate ping scan and reports the highest ping, the tracked player count and the configured limit. | pingkick.admin |
/pingkick status | Prints the current check interval, ping limit and number of consecutive failures required for a kick. | pingkick.admin |
| Permission | Grants |
|---|---|
pingkick.exempt | Player is never kicked for high ping. |
pingkick.admin | Allows the /pingkick chat commands. |
How the code works
Config: limits and timing
The nested
PluginConfigclass holdsMax Ping,Checks Before Kick,Check Interval Seconds,Warn Before KickandKick Reason.LoadConfigreads the file throughConfig.ReadObject<PluginConfig>()and falls back toLoadDefaultConfigwhen parsing fails, thenSaveConfigwrites it back withConfig.WriteObject. Admins tune the values with the file inoxide/configrather than recompiling.OnServerInitializedclamps the numbers to sane minimums (50 ms, one check, one second) and starts the repeating timer withtimer.Every.Sampling average ping
Each tick walks
BasePlayer.activePlayerList, skips NPCs, sleeping and disconnected players, and skips anyone holdingpingkick.exempt, then readsplayer.net?.connection. The average ping comes fromNetwork.Net.sv.GetAveragePing(connection), which covers a window of recent samples instead of one packet. A failed sample (null connection or a non-positive reading) is ignored and does not change the player's counter. The streak count per player is stored in thestreaksdictionary keyed by the ulong fromplayer.userID.Get().Counting consecutive failures
When a sample is at or below
Max Ping, the player's entry instreaksis reset to zero, so a single spike never accumulates toward a kick. When it is above the limit, the count increases. Only once the streak reachesChecks Before KickdoesKickHighPingrun: it sends the configured warning through the language file withMsg, builds the reason string the same way, and callsplayer.Kick(reason)immediately. The streak is zeroed before the kick so a failed kick attempt does not retrigger on every scan.Cleaning up state
OnPlayerDisconnectedremoves the player's id fromstreaksso a reconnect starts with a clean counter. This also matters when an admin or another plugin kicks the player manually. The dictionary holds only ulong keys, no entity references, so nothing is leaked across saves. InUnloadthe repeatingTimerheld inscanTimeris destroyed andstreaksis cleared. No data file is written because a reconnect always starts fresh.Admin commands
/pingkick checkloops the online players, finds the highest current ping and the number of tracked ids, then replies with the counts and the configured limit./pingkick statusprintsCheck Interval Seconds,Max PingandChecks Before Kick. Both are handled by a single[ChatCommand("pingkick")]method that inspectsargs[0]and is guarded bypermission.UserHasPermission(player.UserIDString, "pingkick.admin"). Any other argument prints the usage line. The permission is registered inInittogether withpingkick.exempt.Messages and language
Every string the player sees lives in
LoadDefaultMessagesunder keys likeKick.Warning,Kick.Reason,Command.CheckandCommand.Status.Msgformats them withstring.Formatso{0}becomes the limit, the interval or the failure count. Admins translate by editing the file inoxide/lang/en/PingKick.json. Because the warning is sent just before the kick, the player sees the reason in the client's disconnect dialog through the kick reason, and can reconnect from a better connection.Edge cases that matter
The scan skips
player.IsNpc,!player.IsConnectedandplayer.IsSleeping()to avoid false positives during load screens. A server under load can see the ping reading rise for everyone at once, so the default limit is 350 ms with three consecutive checks. LoweringChecks Before Kickto 1 makes the plugin act like a hard ceiling; raising it to 5 gives more slack for players on unstable links. The interval minimum of one second keeps accidental config typos from lagging the server.
Install it on your server
Save PingKick.cs
Keep the file name exactly as the class name. Oxide matches them.Upload it to the plugins folder
Oxide:oxide/plugins/. Carbon:carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.Edit the config if you want
On first load the default config is written tooxide/config/PingKick.json(Carbon:carbon/configs/PingKick.json). After editing, reload it:oxide.reload PingKick c.reload PingKick # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default pingkick.exempt c.grant group default pingkick.exempt # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Warn-only mode
Never kicks, only warns the player and prints to console when someone crosses the threshold.
Generate this versionConfigurable kick ladder
Warns on early offences and disconnects only after a stored violation count.
Generate this versionConsole report for admins
Prints every kick with the ping value and timestamp so admins can spot recurring offenders.
Generate this version
FAQ
Where does the ping value come from?
Network.Net.sv.GetAveragePing(player.net.connection) returns the server's moving average of recent ping samples for that connection. It is the same number the server browser shows and updates continuously. Because it is an average, one bad packet will not increase it enough to trigger a false kick when the threshold is set sensibly.
Why three consecutive checks instead of one?
Routing hiccups, GC pauses on the client and temporary congestion can spike a single reading. Requiring three failures in a row means a player with a stable connection stays online through a blip, while someone on a genuinely broken link fails every check within seconds. The count resets in streaks as soon as one sample comes back under the limit.
How do I exempt admins?
Grant oxide.grant user <steamid> pingkick.exempt from the server console or create a group with oxide.group add staff and then oxide.grant group staff pingkick.exempt. Exempt players are skipped by the scan before any ping is read, so they never get warnings or kicks.
Does the plugin work on Carbon?
Yes. Carbon loads Oxide plugins from oxide/plugins and exposes the same permission, lang, config and timer APIs this plugin uses. Place PingKick.cs in the plugins folder and reload; the Network.Net.sv call is part of the game assembly, so it behaves the same on either framework.
Will it kick players who are still loading in?
No. The scan skips NPCs, sleeping players and any player whose net.connection is null, which is the normal state on the loading screen. The check only starts counting once the player is fully connected and reporting a valid average ping.
Customize it for your server
Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More moderation plugins
- AFK kickTracks each connected player's movement and look direction with a repeating timer and kicks anyone who stays idle past a configured limit after aโฆOnServerInitializedOnPlayerConnected
- Chat anti-spamBlocks chat messages that exceed a rate limit or duplicate the player's previous message, and mutes repeat offenders for a configurable time.OnPlayerChat
- Chat muteA timed chat mute plugin: /mute <player> <minutes> [reason] stores a mute with an expiry in a data file, /unmute <player> clears it, andโฆOnServerInitializedOnPlayerChat
Last updated Oct 26, 2026.
Customize high ping kick
Customize