Skip to content
RustPluginGenerator

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

PingKick.cs
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.

PingKick.json
{
  "Max Ping": 350,
  "Checks Before Kick": 3,
  "Check Interval Seconds": 30,
  "Warn Before Kick": true,
  "Kick Reason": "High ping"
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/pingkick checkRuns one immediate ping scan and reports the highest ping, the tracked player count and the configured limit.pingkick.admin
/pingkick statusPrints the current check interval, ping limit and number of consecutive failures required for a kick.pingkick.admin
Permissions
PermissionGrants
pingkick.exemptPlayer is never kicked for high ping.
pingkick.adminAllows the /pingkick chat commands.

How the code works

  1. Config: limits and timing

    The nested PluginConfig class holds Max Ping, Checks Before Kick, Check Interval Seconds, Warn Before Kick and Kick Reason. LoadConfig reads the file through Config.ReadObject<PluginConfig>() and falls back to LoadDefaultConfig when parsing fails, then SaveConfig writes it back with Config.WriteObject. Admins tune the values with the file in oxide/config rather than recompiling. OnServerInitialized clamps the numbers to sane minimums (50 ms, one check, one second) and starts the repeating timer with timer.Every.

  2. Sampling average ping

    Each tick walks BasePlayer.activePlayerList, skips NPCs, sleeping and disconnected players, and skips anyone holding pingkick.exempt, then reads player.net?.connection. The average ping comes from Network.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 the streaks dictionary keyed by the ulong from player.userID.Get().

  3. Counting consecutive failures

    When a sample is at or below Max Ping, the player's entry in streaks is 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 reaches Checks Before Kick does KickHighPing run: it sends the configured warning through the language file with Msg, builds the reason string the same way, and calls player.Kick(reason) immediately. The streak is zeroed before the kick so a failed kick attempt does not retrigger on every scan.

  4. Cleaning up state

    OnPlayerDisconnected removes the player's id from streaks so 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. In Unload the repeating Timer held in scanTimer is destroyed and streaks is cleared. No data file is written because a reconnect always starts fresh.

  5. Admin commands

    /pingkick check loops the online players, finds the highest current ping and the number of tracked ids, then replies with the counts and the configured limit. /pingkick status prints Check Interval Seconds, Max Ping and Checks Before Kick. Both are handled by a single [ChatCommand("pingkick")] method that inspects args[0] and is guarded by permission.UserHasPermission(player.UserIDString, "pingkick.admin"). Any other argument prints the usage line. The permission is registered in Init together with pingkick.exempt.

  6. Messages and language

    Every string the player sees lives in LoadDefaultMessages under keys like Kick.Warning, Kick.Reason, Command.Check and Command.Status. Msg formats them with string.Format so {0} becomes the limit, the interval or the failure count. Admins translate by editing the file in oxide/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.

  7. Edge cases that matter

    The scan skips player.IsNpc, !player.IsConnected and player.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. Lowering Checks Before Kick to 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

  1. Save PingKick.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. 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.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/PingKick.json (Carbon: carbon/configs/PingKick.json). After editing, reload it:oxide.reload PingKick c.reload PingKick # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip: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 version
  • Configurable kick ladder

    Warns on early offences and disconnects only after a stored violation count.

    Generate this version
  • Console 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.

Prompt: Create an Oxide plugin named PingKick that scans player ping every 30 seconds, warns and kicks players above 350 ms for three checks in a row, with a pingkick.exempt permission and two admin chat commands.
Customize for my server

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026.

Customize high ping kick

Customize